package broker import ( "fmt" "sort" ) // Streams and consumers derived from what modules declare. // // The mesh's own four exist before any module does (streams.go). Everything here is the other // half: a seat's stream comes into being when the module declaring it is **registered**, and a // consumer when a module is **assigned** — which is why ADR 0116's task 1.4 had to be narrowed to // the foundation set. Neither has happened at genesis. // // All of it is a pure function of declarations. The controller is still the only writer; this is // only what it writes. // A Consumer is a durable subscription the controller creates on a module's behalf. A module // declares what it reacts to, never how delivery works, so it does not name these and cannot // misconfigure them. type Consumer struct { Name string Stream string // Filters are the subjects this consumer receives. One consumer per module with several // filters, rather than one per consumed event: its ack subject is derived from its name, and // a module with five consumers would need five ack permissions to ack its own deliveries. Filters []string // Queue is the queue group, set for a seat's worker so that "exactly one holder" survives a // seat later being relaxed to several. Authority and delivery are kept separate on purpose. Queue string // AckWaitSeconds before an unacknowledged delivery is redelivered. AckWaitSeconds int // MaxDeliver before the message is dead-lettered; zero for the mesh's default. MaxDeliver int Why string } // seatStreamName is the stream holding a seat's inbound work. Named after the seat rather than // the module holding it, because the holder can change and the queued work must not care — which // is the whole reason a caller addresses a seat instead of a module. func seatStreamName(seat string) string { return "SEAT_" + upperSnake(seat) } // SeatStreams is one work queue per declared seat, created when the declaring module is // registered rather than when it is assigned. // // **The stream exists before anyone holds the seat, and that is the point.** Work queues until a // holder appears, so installing the telegram module a week after something started sending to it // flushes the backlog instead of having lost it. A stream created at assignment would make "the // holder is not here yet" mean "your messages are gone". func SeatStreams(seats []DeclaredSeat) []Stream { sorted := append([]DeclaredSeat(nil), seats...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name }) var out []Stream for _, s := range sorted { if len(s.Accepts) == 0 { // A seat that only emits and serves needs no stream: its events ride EVENTS and its // tools are core request/reply, which is never persisted. continue } retain := s.RetainSeconds if retain == 0 { retain = 7 * 24 * 60 * 60 } out = append(out, Stream{ Name: seatStreamName(s.Name), Subjects: []string{"mesh.seat." + s.Name + ".accept.>"}, Retention: RetentionWorkQueue, MaxAge: retain, Why: fmt.Sprintf("work submitted to the %s seat; one holder consumes it, and it "+ "queues while nobody does", s.Name), }) } return out } // A DeclaredSeat is a seat as the catalogue knows it. Mirrored here rather than imported so this // package stays free of the catalogue's own types — the same reason the host mirrors the // contracts instead of importing the sdk. type DeclaredSeat struct { Name string Accepts []string RetainSeconds int } // ConsumerFor is the durable consumer a module's declarations imply, or false when it subscribes // to nothing and needs none. // // One per module, with every consumed subject as a filter, because its ack permission is derived // from its name: a module with a consumer per event would need an ack permission per consumer, // and the permission list would stop being derivable from the declaration. func ConsumerFor(p Principal) (Consumer, bool) { if p.Kind != KindModule || len(p.Consumes) == 0 { return Consumer{}, false } perms, err := PermissionsFor(p) if err != nil { return Consumer{}, false } var filters []string for _, s := range perms.Subscribe { if len(s) > 9 && s[:9] == "mesh.mod." { filters = append(filters, s) } } if len(filters) == 0 { return Consumer{}, false } sort.Strings(filters) return Consumer{ Name: consumerDurable(p), Stream: consumerStream(p), Filters: filters, AckWaitSeconds: 30, MaxDeliver: 5, Why: "what " + p.Module + " declared it consumes; after max-deliver it dead-letters", }, true } // HolderConsumerFor is the worker a seat's holder gets on that seat's work queue. // // **A queue group even though the seat guarantees one holder.** The seat is *authority* — who may // be the telegram sender — and the queue group is *delivery*. Tie delivery to the seat and the // day somebody allows two holders for throughput, every message is processed twice with nothing // reporting it. Kept separate, relaxing one changes nothing about the other. func HolderConsumerFor(node, module string, seat DeclaredSeat) (Consumer, bool) { if len(seat.Accepts) == 0 { return Consumer{}, false } return Consumer{ Name: "SEAT_" + upperSnake(seat.Name) + "_worker", Stream: seatStreamName(seat.Name), Filters: []string{"mesh.seat." + seat.Name + ".accept.>"}, Queue: "holders", AckWaitSeconds: 60, MaxDeliver: 5, Why: fmt.Sprintf("%s on %s holds %s; it acknowledges after the work is done, so a "+ "crash mid-work redelivers rather than loses", module, node, seat.Name), }, true } // AllOverlaps reports subject filters claimed by more than one stream, across the mesh's own and // every derived one. // // NATS refuses an overlapping stream rather than merging it (verified against nats-server 2.10: // "subjects overlap with an existing stream"), so this is not a subtle divergence — it is a // registration that fails. Catching it here names both streams, before a half-applied mesh does. func AllOverlaps(seats []DeclaredSeat) []string { all := append(MeshStreams(), SeatStreams(seats)...) seen := map[string]string{} var clashes []string for _, s := range all { for _, subject := range s.Subjects { if first, ok := seen[subject]; ok { clashes = append(clashes, fmt.Sprintf("%s and %s both claim %s", first, s.Name, subject)) continue } seen[subject] = s.Name } } sort.Strings(clashes) return clashes } // upperSnake makes a stream name from a seat name. NATS stream names may not contain a dot, // a space or a wildcard, and a hyphen is legal but reads badly beside the mesh's own. func upperSnake(s string) string { out := []rune(s) for i, r := range out { switch { case r >= 'a' && r <= 'z': out[i] = r - 32 case r == '-' || r == '.': out[i] = '_' } } return string(out) }