Issue 156: moving a consumer's delivery subject stops a running mesh
146's change is correct on a mesh being raised and fatal on one that is running: the server will not move a push consumer's delivery subject while a subscriber is bound, and a node is bound to its declaration consumer the whole time it is up. Reproduced before fixing; an earlier version of the test unsubscribed first and passed against the broken code. The consumer that works is kept and the assertion says so. Not re-made: the nodes are granted the bare subject only, so re-making would have silenced every machine.
This commit is contained in:
+85
@@ -0,0 +1,85 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-09-29
|
||||
located-in:
|
||||
- mesh-controller internal/broker/jetstream.go (EnsureConsumer)
|
||||
fixed-by: mesh-controller fix/156-a-consumer-that-works-is-not-replaced
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 156 — Moving a consumer's delivery subject stops the control plane, and only on a mesh that is running
|
||||
|
||||
## What was observed
|
||||
|
||||
The control plane crash-looped, every restart ending the same way:
|
||||
|
||||
```
|
||||
mesh-controller: asserting how novox hears its declaration:
|
||||
bringing consumer novox on NODES to match: nats: consumer name already in use
|
||||
```
|
||||
|
||||
It came up on the first build of the controller in eight hours. The machines kept running what they
|
||||
already held — this stops the mesh being *changed*, not the services it placed — and nothing could be
|
||||
pushed, no report was consumed and no enrolment answered, for as long as it lasted.
|
||||
|
||||
## Why
|
||||
|
||||
[Issue 146](../146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md) put the
|
||||
stream into a push consumer's delivery subject, because one process holding two consumers of the same
|
||||
name on two streams was given one subject and acted on every message twice.
|
||||
|
||||
**The server will not move a push consumer's delivery subject while a subscriber is bound to it.** It
|
||||
refuses with `consumer name already in use` — a message about the name, for a conflict about the
|
||||
subject, which is why the trail starts in the wrong place.
|
||||
|
||||
A node is bound to its declaration consumer the whole time it is up. That *is* a node listening for
|
||||
what it should be. So every node consumer in a mesh that is running is one the assertion cannot bring
|
||||
to match — and the assertion happens before the controller serves, so it never serves.
|
||||
|
||||
The controller's own two consumers moved without trouble, and are on the new subject in the live mesh.
|
||||
It asserts them before it subscribes, so nothing was bound.
|
||||
|
||||
## Why nothing caught it
|
||||
|
||||
The change was exercised on a mesh being raised, where every consumer is created rather than updated
|
||||
and nothing is bound to any of them. On that path the code is correct. The test that would have caught
|
||||
it needs a mesh that is already running: an existing consumer, a subscriber still attached, and then
|
||||
the assertion.
|
||||
|
||||
Reproduced exactly that way before the fix — same server version, same stream shape, same consumer —
|
||||
and it fails with the same words as the machine did. An earlier version of the same test unsubscribed
|
||||
first and passed against the code that was crash-looping on the control node.
|
||||
|
||||
## What it is not
|
||||
|
||||
- Not the change it shipped beside. The merge that triggered this build carried
|
||||
[issue 152](../152-a-nodes-plan-failure-silently-drops-its-routed-names/00-report.md)'s fix and three
|
||||
other commits that had never been deployed; this one is 146's.
|
||||
- Not a version difference. The test server and the mesh's broker are both nats-server v2.10.29.
|
||||
|
||||
## How it was fixed
|
||||
|
||||
The consumer that works is kept, and the assertion says so instead of failing.
|
||||
|
||||
**Not deleted and re-made.** Re-making moves the subject, and a holder may not yet be allowed to
|
||||
subscribe to the new one: the wider grant travels in the bus's user list, which the control plane
|
||||
composes and a machine applies minutes later. On the live mesh the nodes are granted `_DELIVER.<node>`
|
||||
and not `_DELIVER.<node>.>` — re-making would have silenced every machine in the mesh, which is worse
|
||||
than the collision it was fixing and far harder to undo. That was the first fix written here, and the
|
||||
permission is the reason it was not shipped.
|
||||
|
||||
**Not fatal**, which is what 146's change intended and did not do: the bare subject still delivers, and
|
||||
collides only where one holder has two consumers of one name. A node has one.
|
||||
|
||||
## How the fix is checked
|
||||
|
||||
Two tests against a real server: a consumer with a subscriber bound keeps its subject, is reported,
|
||||
and still delivers to that subscriber; a consumer with nothing bound moves, so 146's fix still applies
|
||||
where the collision actually was.
|
||||
|
||||
## What is left
|
||||
|
||||
The node consumers stay on the bare subject, which is correct and not tidy. Moving them needs the
|
||||
wider grant to reach every machine first, and then an assertion made while each node is detached —
|
||||
which is its own piece of work, not a side effect of a restart. Nothing is wrong while they do not
|
||||
move: one consumer per name per stream cannot collide with itself.
|
||||
Reference in New Issue
Block a user