Skip to content

Lifecycle, reconnect, and streams

Use async with ECNClient(config) whenever possible. Entry waits for MQTT v5 CONNACK; exit closes streams, task handlers, correlation subscriptions, and the transport within shutdown_timeout.

Watchers are lazy and reference-counted. EventStream.aclose() releases its filter; the MQTT unsubscribe occurs when the final consumer of that filter closes. A negative UNSUBACK is surfaced as a typed error rather than reported as successful broker cleanup. The transport then invalidates the clean-session connection and reconnects only filters that still have local consumers. Streams use bounded buffers with FIFO or latest-value delivery. Overflow does not create an unbounded queue. Each stream exposes dropped_count for local buffer loss and decode_error_count for matching inbound payloads rejected by that watcher’s decoder. Both counters are local diagnostics, not broker-wide metrics. Raw TRACK observations replaced or evicted by all-LATEST ingress coalescing are not decoded-event buffer drops and do not increment dropped_count; their pending identity count and aggregate raw bytes are independently bounded.

The transport reconnects with the configured full-jitter policy and reinstalls only subscriptions that still have local owners. Consumers should tolerate duplicates and gaps, preserve idempotency, and observe client.status.state instead of assuming uninterrupted delivery.

After prior readiness, calling start() again only waits for readiness. A timeout does not replace or close the reconnect supervisor and does not release active watchers; use wait_until_ready() when the caller wants its distinct timeout error.

Cancellation is propagated. Task handlers are async-only and must cooperate with cancellation. Close streams in finally, unregister exact task handlers, and close the client even after startup or handler failure. See cleanup, troubleshooting, and the deployment checklist.

Version 0.2.0 · branch main