Give a work-queue stream its one consumer, drop-only dead letters (issue 1178)
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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"],` +
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user