Skip to content

Converge the two containerised fleet-box stacks (M3's broker.sh --docker vs Ms's compose) - #83

Merged
MJohnson459 merged 1 commit into
mainfrom
fleet-converge-broker
Jul 31, 2026
Merged

Converge the two containerised fleet-box stacks (M3's broker.sh --docker vs Ms's compose)#83
MJohnson459 merged 1 commit into
mainfrom
fleet-converge-broker

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

Converge the fleet box on one containerised broker.

The premise had already moved

Two of the three symptoms in the task were stale. Ms (#63) had already folded
fleet-broker-ws into fleet-broker and pointed compose at
server/mosquitto.conf, so the config was already shared. eclipse-mosquitto:2.0
was never in the repo, and the 18831/19011/18080 ports are from the Ms
verification transcript (shifted +10000 to dodge a live broker), not shipped
config — the containers seen on the machine were that session's.

What was genuinely unconverged: the image tag pinned in two places, a stale
fleet-broker-ws reference in the compose file, docs leading with the
workstation command, and a healthcheck that could not see the failure the whole
websockets story exists to prevent.

Decision: compose is the deployment

broker.sh is the workstation convenience and now defers to it in both
directions — it already used the shared mosquitto.conf, and it now reads the
image tag out of docker-compose.yml instead of carrying a default. A literal
there would be a second pin nobody would remember to bump; if the line is
unreadable it fails loudly rather than falling back. --local resolves nothing
from the deploy directory, so a box with no docker is unaffected.

The tag names a minor series

2.1-alpine, not the floating :2. What must not move under us is how
websockets is built in: 2.0 links libwebsockets, 2.1 implements it natively, and
:2 follows whatever comes next. -alpine is not a variant choice — upstream
publishes 2.1 only as -alpine tags, and :2, :2.1-alpine and :2.1.2-alpine
are one manifest today, so this changes no bytes.

The silent failure is now caught

The broker healthcheck probes 9001 as well as 1883. Mosquitto opens the MQTT
listener and keeps running when the websockets one is refused, so robots and
fleetctl stay healthy while the dashboard has nothing to subscribe to — and
fleet-deploy.sh up gates on this probe, so it would have reported a good
deploy. Measured on a broker with the websockets stanza stripped:

MQTT-only check (old)  -> healthy
both listeners (new)   -> unhealthy

9001 is a TCP probe because the failure being caught is "the listener never
opened", and mosquitto_sub cannot speak websockets to do better.

Verified

  • docker compose up broker --wait passes the new gate on the pinned image;
    both listeners open under 2.1.2.
  • The dashboard's own MQTT codec (server/ui/mqtt.mjs) completes
    CONNECT -> CONNACK -> SUBSCRIBE -> SUBACK over the published WebSocket port.
  • broker.sh resolves the pin, honours a MOTE_BROKER_IMAGE override, and
    fails loudly on an unreadable compose file.
  • New mote_fleet/test/test_deploy_config.py (8 tests, no docker needed) holds
    the invariants: one pin, no second tag anywhere, broker.sh reaching for the
    pinned image against a stub docker, one mounted config, the websockets stanza
    and its strip markers, no log_type override (M3's silent-error guard), both
    ports in the healthcheck, and the published WS port reaching the browser.
    It caught a real duplicate while being written — an example tag in
    broker.sh's error message — which was fixed rather than excused.
  • 194 passed / 6 skipped across mote_fleet; pre-commit clean.

Docs: README.md §6 now leads with compose and presents the pixi tasks as the
same thing in the foreground; server-pipelines.md and CLAUDE.md record the
single pin and the two-listener gate.

NOT PUSHED — no remote writes were made, per the standing git rule. Branch
fleet-converge-broker (commit 3a25ef6) is local; worktree left at
/home/michael/.claude/jobs/7d2e9f4f/wt.

M3 and Ms each grew a way to run mosquitto in a container and did not know
about each other. Ms had already folded the two pixi tasks into one and
pointed compose at `server/mosquitto.conf`, so the config was shared; what
was left was an image tag pinned in two places and docs that led with the
workstation command rather than the deployment.

The compose stack is the fleet box's deployment. `broker.sh` is the
workstation convenience and now defers to it in both directions: it already
used the shared mosquitto.conf, and it reads the image tag out of
docker-compose.yml rather than carrying a default of its own. A literal here
would be a second pin nobody would remember to bump. --local resolves nothing
from the deploy directory, so a box with no docker is unaffected.

The tag names a minor series, `2.1-alpine`, not the floating `:2`. What must
not move under us is how websockets is built in: 2.0 links libwebsockets, 2.1
implements it natively, and `:2` follows whatever comes next. `-alpine` is not
a variant choice — upstream publishes 2.1 only as `-alpine` tags, and `:2`,
`:2.1-alpine` and `:2.1.2-alpine` are one manifest today, so this changes no
bytes.

The broker healthcheck now probes 9001 as well as 1883, because that failure
is silent: mosquitto opens the MQTT listener and keeps running when the
websockets one is refused, so robots and fleetctl stay healthy while the
dashboard has nothing to subscribe to — and `fleet-deploy.sh up` gates on this
probe, so it would have reported a good deploy. Measured on a broker with the
websockets stanza stripped: the MQTT-only check reports healthy, the new one
reports unhealthy. 9001 is a TCP probe because the failure being caught is
"the listener never opened" and mosquitto_sub cannot speak websockets.

README §6 now leads with compose and presents the pixi tasks as the same
thing in the foreground.

Verified: compose broker up --wait passes the new health gate on the pinned
image; both listeners open under 2.1.2; the dashboard's own MQTT codec
(server/ui/mqtt.mjs) completes CONNECT/SUBSCRIBE over the published WebSocket
port. test_deploy_config.py holds the invariants without needing docker — one
pin, no second tag, broker.sh reaching for the pinned image against a stub
docker, one mounted config, the websockets stanza and its strip markers, no
log_type override, and both ports in the healthcheck. 194 passed, 6 skipped
across mote_fleet; pre-commit clean.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdGDhvwcDBmcgK9Gc294th
Comment on lines +90 to +94
assert result.returncode == 0, result.stderr
assert "DOCKER_ARGS: run " in result.stdout
assert pin in result.stdout, (
f"broker.sh ran something other than the pinned {pin}:\n{result.stdout}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion doesn't verify what the docstring claims — it can't detect a divergence between the image broker.sh logs and the image docker run actually receives.

broker.sh prints echo "broker: $IMAGE (docker) ..." to stdout at broker.sh#L88 before invoking docker run "$IMAGE" ... at broker.sh#L94. Since assert pin in result.stdout searches the whole combined stdout, it's already satisfied by that diagnostic echo alone — regardless of what argv the stubbed docker actually received. For example, if "$IMAGE" were accidentally dropped from the docker run invocation, or a typo referenced a different variable there, this test would still pass (the DOCKER_ARGS: run check only proves some docker invocation happened, not which image).

To actually test "which image it reaches for" (per the docstring), the assertion should be scoped to the stub's DOCKER_ARGS: line rather than the full stdout, e.g.:

argv_line = next(l for l in result.stdout.splitlines() if l.startswith("DOCKER_ARGS:"))
assert pin in argv_line.split(), f"docker run was handed something other than {pin}: {argv_line}"

@MJohnson459
MJohnson459 merged commit d3da3b2 into main Jul 31, 2026
5 checks passed
@MJohnson459
MJohnson459 deleted the fleet-converge-broker branch July 31, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant