gobridge

0003 — MQTT persistent-session subscription hygiene

Status: accepted Date: 2026-07-03 Deciders: GoBridge core

Context

The paho MQTT adapter defaults to clean_start=false so a shared session survives reconnects and keeps its subscriptions. Combined with manual acks, that persistence creates a hazard: a subscription left on the broker from a previous configuration keeps delivering messages to the session, but the current routing plan has no binding for that topic. Those messages have nowhere to go.

If the adapter buffers them, an orphan subscription can back-pressure and stall the shared session for every route on it. If it acks and drops them blindly, it throws away messages that a still-loading subscription was about to cover. The adapter cannot tell a genuinely orphaned topic from one whose subscription has not been reconciled yet.

The design intent is documented in the adapter (adapters/mqtt/transport/paho/acl_router.go, session_reconcile.go, and the package doc.go).

Decision

Bound the ambiguity with a startup grace window, then reconcile orphans on evidence, guarding against removing subscriptions the plan still wants.

Consequences

Rejected alternatives

Addendum 2026-07-10: covered QoS 1 and 2 retention past grace

Provenance: the covered-retention implementation (MQTTRouterCoveredRetained and the covered-topic past-grace split) landed in commit 438139a (2026-07-10) and was later refactored in commit 4d8d76d (2026-07-13); this addendum text was written in commit 9d8effb (2026-07-10). The date above is the addendum’s commit date.

The original Ack-and-drop past grace decision above described an unconditional ack-and-drop once the grace window elapses. That is no longer accurate. A later hardening split the past-grace path by whether the current routing plan still covers the publish’s topic:

So “ack-and-drop past grace” now applies only to a topic no current route covers; a covered topic is held, not dropped. This closes the loss window where an early publish for a still-reconciling QoS 1/2 subscription could be discarded. The metric semantics are documented on the MQTT transport page (see the covered-retention release note) and in release-notes.md.

On line numbers. The file.go:NN offsets originally cited in the body of this ADR have been dropped: they rot as the code moves and were already stale by this addendum. Cite the named files and symbols (e.g. graceLoop, unsubscribeOrphan, topicCoveredLocked) as the stable reference. New ADRs should follow the same rule — files and symbols, not line numbers.

Addendum: durable exact-filter migration

Persistent/exclusive sessions no longer use concrete delivered topics to infer wildcard/shared filters. They require a durable managed-subscription ledger and remove exact historical filters before dispatch. The legacy exact-topic orphan cleanup described above remains an Ephemeral-session behavior.

A successful UNSUBACK is not sufficient evidence that an unacknowledged shared QoS 1/2 delivery was redistributed. Brokers may pin it to the persistent ClientID. GoBridge now retains history through reconnect verification; a matching replay is held unacknowledged and causes terminal fail-closed migration. Operators must restore the exact old identity/configuration and handler, drain the replay, and retry. See the MQTT transport reference and migration runbook.