Finishing the transport seam would open the SDK to hosts that own their MQTT connection #7
Replies: 3 comments
|
Line references verified against Where this stands after 0.14.0Since this was opened, 0.14.0 landed The half that remains is the opposite case: an embedder that already has a live MQTT That case is not niche. Any application whose MQTT lifecycle is owned elsewhere hits it — an The gap, precisely
The asymmetry is not only the parameter. 2255 if self.mqttc: # _connect_broker
2256 return # injected → the start() at :2265 is unreachable
2857 if self._owns_client: # mqttc is None, fixed at construction
2858 self.mqttc.stop()
What the contract would beDraft PR #12 adds A
So the natural shape is a second protocol deriving from One wart in that shape, and it is the same objection we raise against a flat eight-member The question from the original post, still openRestating because it gates the shape rather than following from it: may an injected client Current behaviour already leans permissive — the sole call site comments that it "is bounded Worth noting #12 cannot answer this even by accident: The alternative, if you would rather not commit to it: Two smaller things in the same areaNeither is worth its own thread, and both are visible from the same code:
One downstream consequence, for completenessNot a request — recording it so the full scope is visible now rather than arriving later.
Offer, unchangedStill glad to prototype whichever direction interests you and put it up as a branch or draft |
|
Both comments and #12 read. Line refs below are current #12: rename the type, then merge
So #12 becomes: rename its three-member The flush question, answeredMay an injected client implement But I'd rather the ownership flag carry it than a docstring. For a non-owned client, The piece the call-site method can't seeTyping by what the SDK calls on the client is correct, but it is blind by construction to what the SDK registers and configures at connect, because those are not calls on the client. Two of them are
So a The shape I'd want, and it mirrors what 0.15.0 already does for the disconnect hook ("a BYO caller registers disconnect handling on its own client"):
That keeps the transport protocol to the four read/write members above (no The same shape, on the consumer side, is narrower than it looksFor completeness, since it is the same The inbound seam, when it landsAgreed that Two I'm not taking
OfferYes, please prototype the producer seam, with the will/republish contract above and the |
|
Taking the rename first: The flush answer I like better than the question. Having One thing from your consumer-side section I want to underline rather than let pass: the Designing the producer seam before building it — one constraint you should see firstThis is the constraint I hit when I sat down to design the prototype you asked for. It bears on Your framing is right — the will is part of CONNECT and in the injected model the caller owns Concretely, the blocking shape is a host whose MQTT layer is single-instance by design, sets A producer injected with such a connection would satisfy the Device contract completely, look The other half of your contract lands verbatim on that same shape of host, which is the The deeper constraint: the host's API caps what an injected producer can expressThe will is the visible instance of something more general, and it is worth naming because it
Worth saying plainly because I nearly proposed expiry as the answer here: it is not available There is a point in your favour buried in that. What does survive, and what it coversThe caller watches the connection and tells the SDK. Not a will substitute — when the Permanent death is the case with no clean answer on such a host. The host's own will fires Expiry is still worth having — just for a different producer. For any producer that owns I checked this rather than trusting the spec text, on Mosquitto 2.1.2 — two retained Costs: it gives Why this matters to us, given we are not the producerOur path is a consumer that turns eBus into HA discovery, so we never publish a It is also why expiry appeals from where we sit: an evaporated Where a capability declaration comes in
Row three is the middle ground and the only genuinely new one. The host's will fires on Its costs, plainly, because there are two: a non-eBus Homie consumer still reads Worth weighing against how bad the residue actually is. The stale One option I will name so you need not wonder why it is absent: an eBus consumer could On the prototypeYou offered it either way, so I will delay that effort, and I would rather clear the logging PR from #11 that I already owe you. That is not a retreat from the analysis — it stands, but three things point the same direction. The shape is not settled: the One thing I should be explicit about, since we opened this thread and you would be entirely That is a change since the thread opened rather than something true all along. We settled on If you would still rather I built it now, say so and I will — but please answer the will question first, |
Uh oh!
There was an error while loading. Please reload this page.
Today the SDK can be embedded only in an application willing to let it own a paho client and a
network thread. That's a fine default, but it's currently the only option, and it rules out hosts
that would otherwise be natural consumers:
Device.stop()blocks up to ~3s on the caller's thread(
publish_and_flushat:1449, thenmqttc.stop(timeout=2.0)at:1455). On an event loopthat's a stalled loop.
component that stalls the loop for seconds isn't shippable there. Its MQTT integration is also
single_config_entry, so a second SDK-owned connection isn't available even in principle — theconnection has to come from the host.
eBus behaviour with no broker and no paho mocking at all.
None of that needs a new concurrency model. It needs the seam you've already started to be finishable
from the outside. The cleanliness is a side effect; the point is who can use it.
You already have two halves of this
Property.async_loop(homie.py:464,:914)/setcallbacks go viaasyncio.ensure_future(..., loop=self.async_loop)when supplied, else are called directly. But it lives onProperty— not plumbed fromDeviceorNode, so a tree with 200 properties sets it 200 times. Also declaredOptional[asyncio.SelectorEventLoop] = False, abooldefault against a non-boolannotation, whichpy.typednow surfaces downstream.Controller.mqttc(0.13.0)Devicehas no equivalent, so a producer in a host that already owns a connection must cede the socket.ha/bridge.py:132,:185threading.Timerfor reconcile, inside the HA-facing module.Plus
property.py'sLock/RLock, which exist to be safe against paho's callback thread — a costthat only earns its keep while the SDK owns the threading model.
The concrete gap
Device.__init__:1225already rejectsparent=withmqtt_cfg=("children share the root's MQTTconnection"), so a root device is the single place a connection enters a tree — the natural insertion
point.
Not only a parameter.
Controllerhonours "an injected client is not started or stopped by theSDK" because both calls are guarded (
startat:2238inside thefrom_config(...)branch,stopat
:2831behindif self._owns_client).Devicehas no such guard:And one design question I'd rather you answered than I assumed: with a caller-owned client, the
caller implements
publish_and_flush— so should an injected client be documented as permitted toimplement it non-blocking (returning
Falsefor "not confirmed"), or shouldstop()grow a way toskip the flush for callers that can't block?
A range of scopes — your call
mqttc=onDeviceplus the ownership flag. Strictly additive:mqttc=Nonedefault,so every existing caller takes today's path unchanged, and the new branch is unreachable without
injection. No MQTT behaviour changes, nothing existing moves. This alone unlocks the async-host
cases above.
async_looppromoted offPropertyto the root,
mqttcalongside it,threading.Timerbehind the same seam. Larger, and it wouldtouch existing signatures.
caller. The pure form, and a rewrite of
homie.pythat breaks every consumer. Alpha is the onlytime it's cheap, but it's your cost.
I'm not arguing for 3 — mentioning it so the range is visible and 1 isn't chosen by default merely
because it's what someone turned up asking for.
Offer
Since this isn't a PR-up-front situation: happy to prototype whichever direction interests you and
put it up as a branch or draft PR to look at, rather than something you're asked to merge. Equally
happy to leave it with you, or to hear the current shape is deliberate and the adapting belongs on
our side — that's a useful answer too.
All reactions