Derive a seat's stream and a module's consumer, and wire JetStream
Task 3.9's other half and 1.4's missing client. The derivation is pure and unit-tested; only "does the server accept this" needs one running, behind MESH_TEST_NATS so the ordinary suite stays offline. A seat's work queue is created at registration, not assignment, so work queues until a holder appears — a stream created at assignment would make "the holder is not here yet" mean "your messages are gone". Named after the seat, because the holder can change and the queued work must not care. A holder's worker uses a queue group even though the seat guarantees one holder: the seat is authority, the queue group is delivery, and tying them together means the day somebody allows two holders every message is processed twice with nothing reporting it. One consumer per module carrying every filter, because its ack permission is derived from its name. And a real bug the live server caught: a durable name may not contain a dot, but an ack subject is $JS.ACK.<stream>.<consumer>, so the single string that read correctly inside the permission was rejected as a consumer name. Split in two, beside the permission that has to match. Unfixed, the symptom would have been every message redelivered forever with a permission list that looks right — which is the failure design 25 §4 warns about.
This commit is contained in:
+29
-8
@@ -203,7 +203,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// module received would be redelivered forever, refused by the permission list it already
|
||||
// has (design 25 §4). Scoped to this principal's own consumer name, so it can ack its own
|
||||
// deliveries and no other's.
|
||||
pub = append(pub, "$JS.ACK."+consumerName(p)+".>")
|
||||
pub = append(pub, "$JS.ACK."+consumerStream(p)+"."+consumerDurable(p)+".>")
|
||||
}
|
||||
|
||||
sort.Strings(pub)
|
||||
@@ -232,17 +232,38 @@ func seatSubject(s Seat, kind, verb string) string {
|
||||
return "mesh.seat." + s.Name + "." + kind + "." + verb
|
||||
}
|
||||
|
||||
// consumerName is the durable consumer the controller derives for this principal. It is here
|
||||
// rather than in the caller because the permission and the consumer must agree by construction —
|
||||
// two places deriving the same name is how a module ends up unable to ack its own deliveries.
|
||||
func consumerName(p Principal) string {
|
||||
// consumerStream and consumerDurable are the two halves of a consumer's identity, and they are
|
||||
// two functions because conflating them was a real bug.
|
||||
//
|
||||
// **A durable name may not contain a dot; an ack subject is built from two names that do.** The
|
||||
// server acknowledges on `$JS.ACK.<stream>.<consumer>.…`, so a single string "EVENTS.one_audit"
|
||||
// reads correctly inside the permission and is rejected as a consumer name — *nats: invalid
|
||||
// consumer name*. Caught against a running server, and worth the comment because the shape of
|
||||
// the failure if it had not been is the one design 25 §4 warns about: a consumer that cannot ack
|
||||
// has every message redelivered forever, and its permission list looks right while it happens.
|
||||
//
|
||||
// They are derived here, beside the permission that must match them, because two places deriving
|
||||
// the same name is how a module ends up unable to ack its own deliveries.
|
||||
func consumerStream(p Principal) string {
|
||||
switch p.Kind {
|
||||
case KindModule:
|
||||
return "EVENTS." + p.Node + "_" + p.Module
|
||||
return "EVENTS"
|
||||
case KindNode:
|
||||
return "NODES." + p.Node
|
||||
return "NODES"
|
||||
case KindController:
|
||||
return "CONTROL.controller"
|
||||
return "CONTROL"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func consumerDurable(p Principal) string {
|
||||
switch p.Kind {
|
||||
case KindModule:
|
||||
return p.Node + "_" + p.Module
|
||||
case KindNode:
|
||||
return p.Node
|
||||
case KindController:
|
||||
return "controller"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user