Fire-and-forget senders
Send without waiting for replies, with buffered events delivered when the connection stops.
Wrap a sender that expects no reply - an audit trail, a metrics collector, a notification gateway - so that services append events to a client-side buffer and continue without waiting. A background loop delivers the buffer in batches and, when the connection stops because its definition is deleted or edited, the remaining events are flushed before the client closes.
The buffering is ordinary client code. The Connector SDK supplies the lifecycle - on_stop runs when the connection stops, so the flush-on-stop is one line.
The connector module
The connector wraps a client for an audit collector. Events accumulate in the client's buffer, a background loop flushes them in batches and close - called from on_stop - sends the remainder:
# -*- coding: utf-8 -*-
# stdlib
import socket
import threading
import time
# Zato
from zato.common.sdk import Connector, Field
class AuditClient:
""" A fire-and-forget sender for an audit collector - events are buffered client-side
and flushed in batches, each batch over its own short-lived connection, and a failed batch
returns to the buffer for the next flush.
"""
def __init__(self, host:'str', port:'int', flush_interval:'int') -> 'None':
self.host = host
self.port = port
self.flush_interval = flush_interval
# Guards the buffer.
self.lock = threading.Lock()
# Events waiting for the next flush.
self.buffer = []
# Set by close, which stops the flusher loop.
self.is_stopped = False
# Flush periodically in the background.
flusher_thread = threading.Thread(target=self._flush_loop, daemon=True)
flusher_thread.start()
def _flush_loop(self) -> 'None':
while not self.is_stopped:
# Waiting in small slices makes close take effect quickly even with long intervals.
waited = 0.0
while waited < self.flush_interval and not self.is_stopped:
time.sleep(0.1)
waited += 0.1
if self.is_stopped:
return
try:
self.flush()
except OSError:
# The collector is down - the events stay in the buffer for the next flush.
pass
def add(self, event:'str') -> 'None':
with self.lock:
self.buffer.append(event)
def flush(self) -> 'None':
# Take the whole buffer under the lock ..
with self.lock:
batch = self.buffer[:]
self.buffer.clear()
# .. an empty batch means there is nothing to send.
if not batch:
return
# .. and send it over one short-lived connection.
payload = ''.join(f'{event}\n' for event in batch)
try:
with socket.create_connection((self.host, self.port)) as conn:
conn.sendall(payload.encode('utf8'))
except OSError:
# The collector is down - put the batch back for the next flush.
with self.lock:
self.buffer[0:0] = batch
raise
def close(self) -> 'None':
""" Stops the flusher and sends the events that remain in the buffer - the flush-on-stop.
"""
self.is_stopped = True
self.flush()
class AuditConnector(Connector):
""" Wraps the audit collector as a connection type that services access through self.out.audit.
"""
type = 'audit'
# Configuration schema
host = Field.Text()
port = Field.Int(default=9990)
flush_interval = Field.Int(default=2)
def create_client(self) -> 'AuditClient':
return AuditClient(self.config.host, self.config.port, self.config.flush_interval)
def ping(self, client:'AuditClient') -> 'None':
# The collector sends no replies, so the check is that the socket opens.
conn = socket.create_connection((client.host, client.port))
conn.close()
def on_stop(self, client:'AuditClient') -> 'None':
client.close()
def send_event(self, event:'str') -> 'None':
self.client.add(event)
send_event only appends to the buffer, so services return immediately, without waiting for the collector.
Each batch travels over its own short-lived connection and a failed batch returns to the buffer, so a collector that is down for a while receives the events on a later flush.
on_stop runs when the definition is deleted or edited and when the server shuts down. Calling close there delivers the buffered remainder.
Create a definition
Create the definition in an enmasse file, under the custom_audit key - custom_ followed by the connector's type:
Call the connection from a service
The service appends one event to the buffer and returns - the background loop delivers it later:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class SendAuditEvent(Service):
""" Sends one fire-and-forget event to the audit collector.
"""
name = 'demo.audit.send-event'
input = 'event'
def handle(self) -> 'None':
conn = self.out.audit['My Audit']
conn.send_event(self.request.input.event)
Services append events and continue - the client delivers them in batches, and the flush in on_stop delivers the remainder when the connection stops.
See also
| Page | What it covers |
|---|---|
| SDK reference | Field types, lifecycle methods and the behavior of every invocation |
| Server push and subscriptions | Traffic in the other direction - messages the remote side pushes |
| Connector SDK tutorial | Build a complete connector and call it from a service |