Give a work-queue stream its one consumer, drop-only dead letters (issue 1178)

This commit is contained in:
jochen
2026-10-19 02:33:22 +02:00
parent 97ee33e8f4
commit de651e0fd1
16 changed files with 467 additions and 22 deletions
+1
View File
@@ -1953,6 +1953,7 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) {
problems = append(problems, LogProblems(m)...)
// And the streams it owns, and who may hear them (streams.go, novox/hq ADR 0363).
problems = append(problems, StreamProblems(m)...)
problems = append(problems, WorkQueueStreamProblems(m)...)
// And the defaults it gives its settings (setting_defaults.go, novox/hq ADR 0262), or, written in the
// config form, the whole declaration of each value (config.go, novox/hq ADR 0348).
if m.DeclaresConfig() {
+5 -2
View File
@@ -18,10 +18,13 @@ func mailSenderProcessManifest(t *testing.T, bus string) string {
process += `,"bus":` + bus
}
process += `}`
// The stream itself is plain here, not retention work-queue: 1179 checks only that a process names a
// stream the module owns, never its retention — that rule is 1178's (WorkQueueStreamProblems), and is
// tested against its own fixture there (work_queue_consumer_test.go) so this one stays accepted whether
// or not a process currently names "mail-sends" in bus.queues.
return `{"module":"mesh-mail","version":"1","emits":["mail.send.sent","mail.send.failed"],` +
`"state":["sends"],"streams":[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,` +
`"max-mib":64,"retention":"work-queue","max-deliveries":5,"duplicates":24}],` +
`"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` + process + `]}`
`"max-mib":64}],"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` + process + `]}`
}
const mailSenderBus = `{"emits":["mail.send.sent","mail.send.failed"],"queues":["mail-sends"],` +
+2
View File
@@ -280,6 +280,8 @@ func CatalogueProblems(shelf Shelf) []string {
problems = append(problems, StreamNameProblems(shelf)...)
// A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2).
problems = append(problems, RestrictedHearingProblems(shelf)...)
// A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6).
problems = append(problems, WorkQueueConsumedElsewhereProblems(shelf)...)
// Who declares what, and who declared it first.
declaredBy := map[string]string{}
+7 -2
View File
@@ -13,9 +13,14 @@ import (
const mailSendsStream = `[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,"max-mib":64,` +
`"retention":"work-queue","max-deliveries":5,"duplicates":24}]`
// 1177/AC1: a well-formed work-queue stream is accepted and read back whole.
// 1177/AC1: a well-formed work-queue stream is accepted and read back whole. Named by one process in
// bus.queues (novox/hq ADR 0436 §6, checked by 1178's WorkQueueStreamProblems), so this stays accepted once
// that rule exists.
func TestAWorkQueueStreamIsDeclaredAndAccepted(t *testing.T) {
m, err := ParseManifest([]byte(mailManifest(t, mailSendsStream, "")))
m, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","streams":` + mailSendsStream +
`,"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` +
`{"id":"send","type":"process","run":["./mesh-mail","send"],"user":"mesh-mail-send",` +
`"bus":{"queues":["mail-sends"]}}]}`))
if err != nil {
t.Fatalf("a well-formed work-queue stream was refused: %v", err)
}
+58
View File
@@ -351,6 +351,64 @@ func patternsOverlap(a, b string) bool {
}
}
// WorkQueueStreamProblems refuses a work-queue stream (novox/hq ADR 0436 §6) that is not read by exactly
// one process of its own module: named by none, it queues forever unconsumed; named by two, each would take
// half of what the other needed, silently.
func WorkQueueStreamProblems(m Manifest) []string {
var problems []string
named := map[string]int{}
for _, b := range m.ProcessBuses() {
for _, q := range b.Queues {
named[q]++
}
}
for _, s := range m.Streams {
if s.Retention != StreamRetentionWorkQueue {
continue
}
switch named[s.Name] {
case 0:
problems = append(problems, fmt.Sprintf(
"%s's work-queue stream %q: no process names it in bus.queues; a work-queue stream is read by "+
"exactly one process (novox/hq ADR 0436 §6)", m.Module, s.Name))
case 1:
default:
problems = append(problems, fmt.Sprintf(
"%s's work-queue stream %q: %d processes name it in bus.queues; a work-queue stream is read by "+
"exactly one (novox/hq ADR 0436 §6)", m.Module, s.Name, named[s.Name]))
}
}
return problems
}
// WorkQueueConsumedElsewhereProblems refuses another module's `consumes` naming a work-queue stream's events
// (novox/hq ADR 0436 §6): read by its module's one process alone, never broadcast to a consumer that only
// subscribes. Modelled on RestrictedHearingProblems.
func WorkQueueConsumedElsewhereProblems(shelf Shelf) []string {
var problems []string
for _, module := range shelfOrder(shelf) {
m := shelf[module]
for _, c := range m.Consumes {
emitter, rest, named := strings.Cut(c, ".")
provider, known := shelf[emitter]
if !named || emitter == OneName || emitter == TheRest || !known || emitter == module {
continue
}
hearing, _ := provider.StreamsHearing(rest)
for _, s := range hearing {
if s.Retention != StreamRetentionWorkQueue {
continue
}
problems = append(problems, fmt.Sprintf(
"%s consumes %s, which %s's work-queue stream %s carries; a work-queue stream is read by "+
"its module's one process alone, never by another module's consumes (novox/hq ADR 0436 "+
"§6)", module, c, emitter, s.BusName()))
}
}
}
return problems
}
// StreamNameProblems are the rules about stream names no single manifest can be judged against: two
// modules declaring one name.
func StreamNameProblems(shelf Shelf) []string {
@@ -0,0 +1,75 @@
package catalogue
import (
"strings"
"testing"
)
// A work-queue stream is read by exactly one process of its own module (novox/hq ADR 0436 §6): named once
// in a process's bus.queues, and never asked for by another module's consumes.
// mailSenderManifestWithQueue is the mesh-mail fixture (ADR 0436 §7, §8) with its process's bus.queues set
// as given, so each 1178/AC2 case can vary just that.
func mailSenderManifestWithQueue(queues string) Manifest {
return Manifest{
Module: "mesh-mail", Version: "1", Emits: []string{"mail.send.sent", "mail.send.failed"},
State: []StateDeclaration{{Name: "sends"}},
Streams: []StreamDeclaration{{Name: "mail-sends", Events: []string{"mail.send.approved"},
Retention: StreamRetentionWorkQueue, MaxDeliveries: 5}},
Resources: []map[string]any{
{"id": "account", "type": "user", "name": "mesh-mail-send"},
{"id": "send", "type": "process", "run": []any{"./mesh-mail", "send"}, "user": "mesh-mail-send",
"bus": map[string]any{"emits": []any{"mail.send.sent", "mail.send.failed"}, "queues": queueList(queues)}},
},
}
}
func queueList(q string) []any {
if q == "" {
return nil
}
out := []any{}
for _, s := range strings.Split(q, ",") {
out = append(out, s)
}
return out
}
// 1178/AC2 (named): a work-queue stream no process names is refused.
func TestAWorkQueueStreamNoProcessNamesIsRefused(t *testing.T) {
m := mailSenderManifestWithQueue("")
got := strings.Join(WorkQueueStreamProblems(m), "; ")
if !strings.Contains(got, "mail-sends") || !strings.Contains(got, "no process names it") {
t.Fatalf("an unnamed work-queue stream: %q", got)
}
}
// 1178/AC2 (named twice): a work-queue stream two processes name is refused.
func TestAWorkQueueStreamTwoProcessesNameIsRefused(t *testing.T) {
m := mailSenderManifestWithQueue("mail-sends")
m.Resources = append(m.Resources, map[string]any{"id": "resend", "type": "process",
"run": []any{"./mesh-mail", "send"}, "user": "mesh-mail-send",
"bus": map[string]any{"emits": []any{"mail.send.sent"}, "queues": []any{"mail-sends"}}})
got := strings.Join(WorkQueueStreamProblems(m), "; ")
if !strings.Contains(got, "mail-sends") || !strings.Contains(got, "2 processes name") {
t.Fatalf("a work-queue stream two processes name: %q", got)
}
}
// 1178/AC2 (named once): a work-queue stream exactly one process names is accepted.
func TestAWorkQueueStreamOneProcessNamesIsAccepted(t *testing.T) {
m := mailSenderManifestWithQueue("mail-sends")
if got := WorkQueueStreamProblems(m); len(got) != 0 {
t.Fatalf("a work-queue stream one process names was refused: %v", got)
}
}
// 1178/AC2 (another module's consumes): a work-queue stream another module's consumes asks for is refused.
func TestAWorkQueueStreamAnotherModulesConsumesIsRefused(t *testing.T) {
sender := mailSenderManifestWithQueue("mail-sends")
reader := Manifest{Module: "reader", Version: "1", Consumes: []string{"mesh-mail.mail.send.approved"}}
got := strings.Join(WorkQueueConsumedElsewhereProblems(Shelf{"mesh-mail": sender, "reader": reader}), "; ")
if !strings.Contains(got, "reader") || !strings.Contains(got, "MAIL_SENDS") {
t.Fatalf("another module's consumes of a work-queue stream: %q", got)
}
}