The consume side on NATS, and the window held by the server
The other implementation behind the seam, so the store-window guarantee now has both: one loop, one message at a time, the same window deciding. What differs is where a held message lives, and that is the whole point of the move — the AMQP side keeps an unacknowledged delivery in this process, bounded by the prefetch and lost if the controller stops; this keeps eight bytes saying when the window opened, and the message stays the server's. Checked against a running server, seven claims that reasoning cannot answer: a report is heard and leaves the work queue; one the store cannot take is naked with a delay, stays in the stream, and is recorded when the store returns; one about a superseded declaration is settled without being acted on; one the store never takes is let go once the bound passes; a heartbeat is heard and nothing is persisted; and the enrolment answer reaches the address the request carried in its payload — the test design 25 §2 asks for, so the reason for that field cannot quietly become folklore. Three things the wiring forced into the open: **The controller could not have consumed a module event.** Its permissions granted no event subject to subscribe and no ack subject on the events stream, so every announcement would have been redelivered for ever, refused by the list it already had. Both narrow: each followed subject named, not `mesh.mod.*.>`. **The controller's consumers are not derived.** It files no manifest, so its authority cannot come from a declaration that does not exist; they sit beside the mesh's own streams and are asserted the same way. No max-deliver on CONTROL — the window's bound is the controller's, and a server that dead-lettered first would discard the push the stream exists to protect. **Channels, not callbacks.** The library would run a handler on its own goroutine, and the window's bookkeeping is unlocked because the AMQP loop never had two.
This commit is contained in:
@@ -150,3 +150,79 @@ func Overlaps() []string {
|
||||
sort.Strings(clashes)
|
||||
return clashes
|
||||
}
|
||||
|
||||
// The mesh's own consumers.
|
||||
//
|
||||
// A seat's streams and a module's consumers are derived from declarations (derived.go). These two
|
||||
// are not: **the controller is not a module and files no manifest**, so its authority and its
|
||||
// subscriptions cannot come from a declaration that does not exist. They are named here, where the
|
||||
// mesh's own streams are named, and narrowly — a controller subscribing `mesh.mod.*.event.>` would
|
||||
// hear every event in the mesh, which it has no business doing and which would make its permission
|
||||
// list stop explaining anything.
|
||||
|
||||
// ControllerName is the controller's durable consumer on each stream it reads, and the name its
|
||||
// ack subject is derived from (nats.go: `$JS.ACK.<stream>.controller.>`).
|
||||
const ControllerName = "controller"
|
||||
|
||||
// ControllerFollows are the events the controller reacts to: the catalogue saying a module's
|
||||
// current version moved, and a catalogue that has just started saying it may have missed builds.
|
||||
//
|
||||
// **These carry the local names the manifests hold today**, which still spell an event the way a
|
||||
// routing key on the bus the mesh has does — `module.<module>.<verb>` rather than design 29's bare
|
||||
// verb — so the derived subject names the module twice. It is consistent, and it is what the
|
||||
// catalogue actually publishes, so it is what the controller must listen to. It changes when those
|
||||
// names are converted, and not before: a subscription written against the name design 29 specifies
|
||||
// would be a controller listening to a subject nothing publishes.
|
||||
var ControllerFollows = []string{
|
||||
"mesh.mod.mesh-catalog.event.module.mesh-catalog.upgraded",
|
||||
"mesh.mod.mesh-catalog.event.module.mesh-catalog.catching-up",
|
||||
}
|
||||
|
||||
// MeshConsumers is what the controller consumes, in the order a person reads it.
|
||||
//
|
||||
// **Unlimited redelivery on CONTROL, deliberately.** The store window's bound is the controller's,
|
||||
// not the server's (window.go): a message is held with a nak-and-delay until the controller either
|
||||
// takes it or gives up and says so. A max-deliver here would dead-letter a push that was being
|
||||
// held through a store restart — the exact message the stream exists to protect — some minutes
|
||||
// before the controller had finished deciding about it.
|
||||
func MeshConsumers() []Consumer {
|
||||
return []Consumer{
|
||||
{
|
||||
Name: ControllerName,
|
||||
Stream: "CONTROL",
|
||||
Push: true,
|
||||
AckWaitSeconds: 30,
|
||||
Why: "the controller is the single consumer of what nodes say; explicit ack and no " +
|
||||
"max-deliver, because the store window's bound is the controller's own",
|
||||
},
|
||||
{
|
||||
Name: ControllerName,
|
||||
Stream: "EVENTS",
|
||||
Filters: ControllerFollows,
|
||||
Push: true,
|
||||
AckWaitSeconds: 30,
|
||||
MaxDeliver: 5,
|
||||
Why: "the two events the mesh's own controller reacts to; after max-deliver it " +
|
||||
"dead-letters, because an announcement it cannot act on will not become actionable",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Ensurer is the part of a JetStream connection consumer assertion needs, narrow for the reason
|
||||
// Asserter is.
|
||||
type Ensurer interface {
|
||||
EnsureConsumer(c Consumer) error
|
||||
}
|
||||
|
||||
// AssertMeshConsumers brings the controller's own consumers into being, and says which one failed.
|
||||
//
|
||||
// After the streams, necessarily: a consumer on a stream that does not exist is refused, and the
|
||||
// refusal names the stream rather than the order.
|
||||
func AssertMeshConsumers(e Ensurer) error {
|
||||
for _, c := range MeshConsumers() {
|
||||
if err := e.EnsureConsumer(c); err != nil {
|
||||
return fmt.Errorf("asserting consumer %s on %s: %w", c.Name, c.Stream, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user