Skip to content

fix(mqtt): keep retained state topics accurate - #65

Merged
stakach merged 1 commit into
masterfrom
fix/mqtt-retained-topic-accuracy
Oct 14, 2026
Merged

stakach merged 1 commit into
masterfrom
fix/mqtt-retained-topic-accuracy

Conversation

@stakach

@stakach stakach commented Oct 14, 2026

Copy link
Copy Markdown
Member

Why

Clients that trust the broker's retained state got the wrong picture. Stagehand's hot list alerted on modules that had been deleted, and after a system's modules were reordered it alerted on the old index. Stagehand hides this today by re-checking every alert over REST, which makes it slow. With this fix the broker can be trusted and that re-check can be removed.

Bugs fixed

  • Old index after a reorder: modules were numbered in database order, not the system's module order, so reordering never changed what was published.
  • Topics left behind by deleted or moved modules: the topics to remove were worked out from the module's redis state and its current mapping, and either can be gone by the time the change arrives. Source now records what it has published per module (Published). After any system, module, driver or zone change it removes topics that no longer apply (keeping any another module now owns) and publishes current values at new ones.
  • Two quick edits to one system: they were handled in parallel and could leave the older module list. Changes to a system are now handled one at a time, re-reading the system from the database.
  • Module added after startup: state written before source knew the module's system was never published.
  • Retagging any zone wrote that zone into every system's mappings. Only systems with the zone are changed now.
  • Deletes could overtake publishes: deletions had their own queue that jumped ahead of older publishes, so a stale value could come back. Both now go through one ordered queue.
  • Cleared statuses were retained as null. They are now removed from the broker.
  • Broker restart: a restarted broker held nothing until each value changed. Source now republishes everything on reconnect.
  • Broker down at startup: source gave up on that broker. It now retries with backoff (1s, up to 30s).
  • Stale topics already on brokers: after the initial sync (and each resync), source reads the broker's retained state topics and removes the ones nothing maps to. This cleans up what production brokers hold today.

Behaviour changes

  • Cleared statuses and destroyed models are removed from the broker instead of retained as null.
  • Deletes are no longer sent ahead of earlier publishes.

Testing

  • ./test: 62 examples, 0 failures. New spec/reconcile_spec.cr, plus new cases in the control system and zone router specs.
  • End to end, against a local PlaceOS stack with mosquitto (PlaceOS/user-interfaces, stagehand e2e/local). The specs change systems the way an admin would and require the broker's retained topics to match exactly what the system's modules, names, zones and redis state imply. They cover reorder, removal from a system, module delete, system delete, rename, moving a system to another level and a cleared status. All fail against placeos-2.2610.1 and pass with this branch (15/15 on repeated runs).
  • By hand: a planted stale topic is removed by the startup sweep; after a mosquitto restart all 32 topics were back within 2s; source started with the broker down recovers when it comes up.

Not covered

A status set with a TTL expires out of redis without an event, so the broker keeps its last value.

🤖 Generated with Claude Code

Brokers held retained state that no longer applied, and missed state that
did. Clients that trust the broker (stagehand's hot list) alerted on deleted
modules and on a module's old index after a reorder.

- number modules by the system's own module order, not database order
- record what is published per module, and after any system, module,
  driver or zone change remove topics that no longer apply and publish
  current values at new ones
- handle changes to one system in order, re-reading it from the database
- publish state written before the module's system was known
- only touch the systems a zone change applies to
- send publishes and deletions through one ordered queue
- remove a cleared status from the broker rather than retaining null
- republish everything when a broker reconnects, and retry a broker that
  is down at startup with backoff
- sweep retained state topics nothing maps to after the initial sync

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the type: bug something isn't working label Oct 14, 2026
@stakach
stakach merged commit c96ec09 into master Oct 14, 2026
10 of 11 checks passed
@stakach
stakach deleted the fix/mqtt-retained-topic-accuracy branch October 14, 2026 13:33
stakach added a commit that referenced this pull request Oct 14, 2026
* fix(mqtt): keep startup memory and time flat on large estates

#65 made startup reconcile every Module in the estate in one batch: each
system mapped as the routers loaded queued a reconcile, so the first pass
read every Module's state into memory at once, on top of the initial sync
that already publishes it with backpressure. Mapping each system also
scanned every Module, so loading was quadratic in the estate's size.

- systems read by the initial load are mapped from that row and queue no
  reconcile; the initial sync covers them
- index system modules by system, and the published registry by system,
  so per-system work no longer scans everything
- reconcile reads status names up front and values a Module at a time
- the broker sweep checks each retained topic as it arrives and keeps only
  the stale ones, rather than comparing two sets of every topic

Mapping 20,000 systems of 5 modules: 9.8s before, 0.04s after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: answer scope from the mappings while the routers load

The Module and Driver routers asked the database whether each row was in
scope as they loaded: a Module's systems and their zones, and for a Driver
every one of its Modules, then theirs. Every system and Module is already
mapped by then, so loading asks the mappings instead.

Metadata is broadcast inline rather than from a fiber per model, so the
bounded publisher queues hold loading back instead of thousands of fibers
each holding a model's JSON, and is published once per model rather than
once per top-level zone, as the topic does not depend on the zone. A system
being loaded is mapped from the zones already read with it.

With the rest of this branch and place-labs/resource#20, at 3,000 systems
and 15,000 modules the routers finish loading in 4s (26s on master) and
boot peaks at 73MiB (1.1GiB on master).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(shard.lock): bump placeos-resource to 3.2.0 for keyset loading

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: bug something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant