Skip to content

Commit 049e876

Browse files
authored
docs: a transport must preserve publish order, and why (#48)
MqttDeviceTransport says nothing about ordering, because neither transport the SDK ships with can violate it: paho's own thread and asyncio_driver each pump one client, so publishes reach the wire in the order they were made. A transport written against the protocol directly need not, and one that starts a task per publish() hands ordering to the scheduler. That matters because ordering is a guarantee the SDK maintains on a producer's behalf. A device's $description precedes the $state=ready that vouches for it, and refresh_tree() publishes a device's own $state after the children it announces -- which is what 0.18.1 fixed. Those are quality-of-implementation, not protocol: a consumer must never depend on them, and doc/consuming-a-homie-tree.md says so at length, since publish order does not survive retention. That is precisely why the warning cannot live there. The consumer guide addresses the other party, so it will never reach a transport author, who can silently drop a guarantee the SDK spends effort meeting. Also records the teardown consequence. A publish() that enqueues and returns is entirely legitimate -- every return in the protocol is typed `object` because the SDK discards them -- but Device.stop() publishes the final $state and returns without flushing, so a queueing transport needs a drain point before its client closes, or that message is lost behind it. Raised by @cayossarian on #46, from building a natively-async transport where the hazard is real rather than theoretical: it holds by luck under a fast broker and breaks under a slow first publish. Docs only; no source changed.
1 parent 31f8376 commit 049e876

2 files changed

Lines changed: 12 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan
44

55
## [Unreleased]
66

7+
### Documentation
8+
9+
- README, bring-your-own-transport: a transport must preserve publish order, and why. `MqttDeviceTransport` says nothing about ordering because the two transports shipped with the SDK cannot violate it (paho's thread and `asyncio_driver` each pump one client), but a transport written against the protocol directly can, and one that starts a task per `publish()` hands ordering to the scheduler. The SDK maintains ordering on a producer's behalf — a device's `$description` precedes the `$state=ready` that vouches for it, and `refresh_tree()` publishes a device's `$state` after the children it announces, which is what 0.18.1 fixed — so a transport can silently drop a guarantee the SDK spends effort meeting. Consumers must still never depend on publish order (`doc/consuming-a-homie-tree.md` says so at length, since order does not survive retention), which is exactly why that document cannot warn a transport author: it addresses the other party. Also notes the teardown consequence: a `publish()` that enqueues is legitimate (every return is typed `object` because the SDK discards it), but `Device.stop()` publishes the final `$state` without flushing, so a queueing transport needs a drain point before the client closes. Raised by [@cayossarian](https://github.com/cayossarian) from building a natively-async transport, where the hazard is real rather than theoretical. ([#46](https://github.com/electrification-bus/python-sdk/issues/46))
10+
711
## [0.19.0] — 2026-08-07
812

913
### Added

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ await driver.stop()
8989

9090
The driver module loads lazily and imports only the standard library plus paho, so a thread-mode consumer (or a constrained build such as a Yocto image) never loads it.
9191

92+
**A transport must preserve publish order.** `MqttDeviceTransport` says nothing about ordering, because the two transports the SDK ships with cannot violate it: paho's own thread and `asyncio_driver` both pump one client, so publishes reach the wire in the order they were made. A transport written against the protocol directly need not. In particular, one that starts a task per `publish()` hands ordering to the scheduler.
93+
94+
That matters because ordering is a guarantee the SDK maintains on your behalf. A device publishes its `$description` before the `$state=ready` that vouches for it, and `refresh_tree()` publishes a device's own `$state` after the children it announces (this is what 0.18.1 fixed). Those orderings are quality-of-implementation, not protocol: a consumer must never *depend* on them, and [`doc/consuming-a-homie-tree.md`](doc/consuming-a-homie-tree.md) says so at length, because publish order does not survive retention. But a consumer that is already subscribed does see them, and dropping them silently makes a producer worse for no benefit.
95+
96+
So if your transport hands work onward rather than publishing inline (an `MqttDeviceTransport` over a natively-async client, say, where `publish()` enqueues and returns), **drain a single queue in order** rather than dispatching each publish independently. The failure is invisible in testing: it holds by luck under a fast broker and breaks under a slow first publish.
97+
98+
`publish()` returning without having reached the wire is otherwise entirely legitimate: the protocol types every return as `object` precisely because the SDK discards them. It does mean teardown needs a drain point of your own, because `Device.stop()` publishes the final `$state` and returns without flushing. Close the queue before closing the client, or that last message is lost behind it.
99+
92100
**A producer should own its MQTT connection.** The example above owns a *dedicated* client and connects it itself, so the SDK's Last Will (`$state=lost` on an ungraceful death) works normally: prefer this for any producer whose liveness matters. A *shared* connection owned by a host (Home Assistant is the archetype: one connection, up before your code loads, its single will already spent on the host's own) **cannot carry an eBus will**, because MQTT allows one will per connection. A producer publishing through such a connection therefore never signals ungraceful death: a crash leaves a stale retained `$state=ready`, and consumers render a dead device as alive. Reconnect is still handled (wire `refresh_tree()` to the host's reconnect callback and gate on `is_connected()`), but permanent death is not, and there is no portable substitute (a host that owns the connection also will not forward the MQTT 5 publish properties that would let `$state` expire). So do not publish a liveness-bearing device through a connection you do not own: if a host environment forbids a dedicated connection, run the producer as a **separate adapter** with its own connection rather than borrowing the host's. (The injected-client seam is still the right tool for the *consumer* role, `Controller(mqttc=...)`, which has no `$state` and no will to lose, and for tests.)
93101

94102
#### Clearing a value vs. an empty-string value

0 commit comments

Comments
 (0)