test+docs(mqtt): end-to-end Bad-then-identical-replay recovery
This commit is contained in:
@@ -415,6 +415,41 @@ one session, and carrying it across made the host's re-subscribe resolve an empt
|
||||
SUBSCRIBE at all, and leave the driver Connected/Healthy and permanently deaf. Sparkplug's
|
||||
`SparkplugIngestor.AttachTo` resets its own session state for the same reason.
|
||||
|
||||
## Quality on connection loss and re-subscribe
|
||||
|
||||
The quality contract this implements is stated once, in
|
||||
[`docs/DriverLifecycle.md` §"The quality contract"](../DriverLifecycle.md#the-quality-contract).
|
||||
Two edge behaviours make MQTT — a push-model driver — honour it. A poll driver gets both free from
|
||||
`PollGroupEngine` (a fresh subscription's first poll runs with `forceRaise: true`); MQTT had neither,
|
||||
which is archreview 05/STAB-18.
|
||||
|
||||
- **Connection loss stales, keeping the value.** On the `Connected → Reconnecting` edge
|
||||
`MqttConnection.ConnectionLost` fires, and both ingest paths mark every **already-observed**
|
||||
reference `BadNoCommunication` with its **last value retained** and publish the change. A
|
||||
never-observed reference (still `BadWaitingForInitialData`) and an already-Bad one are left alone.
|
||||
The event does **not** fire for a failed *initial* connect or during dispose — neither is an outage.
|
||||
- **Every (re)subscribe pushes the cached snapshots.** `SubscribeAsync` republishes each observed
|
||||
reference's cached snapshot onto the new handle before any broker traffic. This is what refills a
|
||||
node an address-space rebuild reseeded `BadWaitingForInitialData` on a quiet topic — a rebuild
|
||||
triggers no retained replay and, in Sparkplug, no rebirth.
|
||||
|
||||
**Why the stale is what makes recovery work.** The plain path suppresses a message whose value *and*
|
||||
status equal the driver-owned cache, and that cache survives reconnects and redeploys. So the retained
|
||||
message a broker replays on reconnect — routinely the identical payload — was suppressed, and every
|
||||
node the host had latched Bad stayed Bad until the plant value happened to change. Staling the status
|
||||
first makes the identical replay a change. (A regression test for this must replay the **identical**
|
||||
payload; a different value defeats the suppression on its own and proves nothing.)
|
||||
|
||||
**Two "not communicating" codes, deliberately.** `BadNoCommunication` (0x80310000) means *the link is
|
||||
down* — the connection-loss stale, and what the host's comms-loss fan-out writes onto the value nodes.
|
||||
`BadCommunicationError` (0x80050000) keeps its narrower Sparkplug meaning: the **source announced its
|
||||
own death** (NDEATH/DDEATH), or a birth voided the scope feeding the tag. A refused SUBSCRIBE filter
|
||||
also degrades to `BadCommunicationError`.
|
||||
|
||||
**Operator-visible change:** an MQTT tag now goes Bad when the broker session drops, rather than
|
||||
holding its last value at Good until the host's 90 s comms-loss watchdog notices — which, since
|
||||
MQTTnet usually reconnects well inside that window, in practice meant never.
|
||||
|
||||
## Write-through
|
||||
|
||||
`IWritable` landed with [#508](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/508). Before it,
|
||||
|
||||
Reference in New Issue
Block a user