diff --git a/modules/messenger/README.md b/modules/messenger/README.md index 0fbd1a41..f2927219 100644 --- a/modules/messenger/README.md +++ b/modules/messenger/README.md @@ -10,8 +10,8 @@ channel's client and no channel's secret. a record each asker reads under its own name), and the two kinded benches its channels hold, `channel` (`show`, `edit`, `send`, each under the holder's kind) and `intake` (`choice`, `link`, `failed`, `standing`, and the proof `code`, request and reply only). Only `channel` and `intake` may be kinded. -- **Reads the controller's open conditions** (`seat:mesh-controller.conditions`) and **consumes its - condition events**; the events are read in one place, `cmd/messenger/condition.go`. +- **Reads the condition keeper's open conditions** (`seat:mesh-conditions.conditions`) and **consumes its + condition events** (the controller's are no longer heard, issue 774); the events are read in one place, `cmd/messenger/condition.go`. - **The channels are modules of their own**: `telegram` (kind `telegram`, reaches the operator away, proves its sender) and `desk-channel` (kind `desktop`, the machines' notifiers, proves nothing). Which kinds exist and what each promises is the controller's word, in this module's membership; whether each can send now @@ -94,7 +94,7 @@ handed over three hours of raised-and-cleared conditions at once, and each was s what is said is decided by the state now and by when a thing happened, never by when it arrived: - **The state first.** On start, every 10 min, and at once when an old event arrives, the holder reads - what is open now from the controller (`mesh-controller.conditions`). An event older than that + what is open now from the condition keeper (`mesh-conditions.conditions`). An event older than that reading is already in it and is not acted on. What is open here and not there ended meanwhile: it is forgotten quietly (Telegram's message is edited, which notifies nobody; the desktop is not woken). An urgent condition open and never said is said — all of them in **one** message. A warning raised in diff --git a/modules/messenger/cmd/messenger/adr505_bus_test.go b/modules/messenger/cmd/messenger/adr505_bus_test.go index 254babc8..19425289 100644 --- a/modules/messenger/cmd/messenger/adr505_bus_test.go +++ b/modules/messenger/cmd/messenger/adr505_bus_test.go @@ -19,7 +19,7 @@ func (b *conditionBed) controllerEvent(t *testing.T, event string, body any) { if err != nil { t.Fatal(err) } - if _, err := b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": ControllerSeat + "." + event, + if _, err := b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": ConditionSeat + "." + event, "node": "test", "body": json.RawMessage(raw)}}); err != nil { t.Fatal(err) } diff --git a/modules/messenger/cmd/messenger/asks.go b/modules/messenger/cmd/messenger/asks.go index 1fd236c8..32160014 100644 --- a/modules/messenger/cmd/messenger/asks.go +++ b/modules/messenger/cmd/messenger/asks.go @@ -479,8 +479,8 @@ func answerLevel(a asks.Ask) asks.Level { return asks.Acknowledge } -// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which -// raises the conditions. +// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which raised the +// conditions; the condition keeper (ConditionSeat), which raises them now, is one too. const ConditionOwner = "mesh-controller" // TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus @@ -651,7 +651,7 @@ func (h *Holder) recordFor(asker, key string, a asks.Ask) string { defer h.mu.Unlock() // Shown on a condition's message only when asked by the condition's owner: another asker naming a // condition's key does not take its message over. - if a.About != "" && asker == ConditionOwner { + if a.About != "" && (asker == ConditionOwner || asker == ConditionSeat) { if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" { return r.Key } diff --git a/modules/messenger/cmd/messenger/condition.go b/modules/messenger/cmd/messenger/condition.go index 7a218fad..60233d1b 100644 --- a/modules/messenger/cmd/messenger/condition.go +++ b/modules/messenger/cmd/messenger/condition.go @@ -1,14 +1,14 @@ package main -// The controller's condition events, read in this one place (novox/hq to-be 45 §2). +// The condition keeper's condition events, read in this one place (novox/hq to-be 45 §2, ADR 0564 clause 9). // -// **The contract this reads, and every assumption it makes about it**, because the controller's +// **The contract this reads, and every assumption it makes about it**, because the keeper's // side was built at the same time from the same design and the design names the events and the -// fields but not their spelling on the wire: +// fields but not their spelling on the wire (the controller's own events are no longer heard, issue 774): // -// - The events are the mesh-controller seat's own: `condition-raised`, `condition-changed` and -// `condition-cleared`, consumed as `mesh-controller.` (subject -// `mesh.seat.mesh-controller.event.`), exactly as `applied` and `built-before` are. +// - The events are the seat mesh-conditions': `condition-raised`, `condition-changed` and +// `condition-cleared`, consumed as `mesh-conditions.` (subject +// `mesh.seat.mesh-conditions.event.`). // - The body is the condition as the store holds it, one JSON object, with the field names of the // design's table in kebab-case: key, kind, subject, severity, summary, evidence, source, raised, // last-observed, observations, tried, resolver, silenced, epoch. snake_case and camelCase @@ -27,7 +27,7 @@ package main // - Every event carries `at`, when the transition happened (the controller's conditions.Event). // It is how this holder tells news from history: a stream replays what it holds to a consumer // that is new or was away, and an event's delivery time says nothing about when it happened. -// - The `conditions` verb of the controller's seat answers `{conditions: [...], open, note}`, each +// - The `conditions` verb of the keeper's seat answers `{conditions: [...], open, note}`, each // entry the condition as the store holds it — the same shape as an event's body. // // An event this cannot read is refused by name — which event, which field, why — counted, said in @@ -40,15 +40,16 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/health" ) -// The events, by their local names under the controller's seat. +// The events, by their local names under the keeper's seat. const ( EventRaised = "condition-raised" EventChanged = "condition-changed" EventCleared = "condition-cleared" - // ControllerSeat is the role the events are stated under. - ControllerSeat = "mesh-controller" + // ConditionSeat is the role the events are stated under, and the verb `conditions` is asked of. + ConditionSeat = health.KeeperModule ) // Severities: two levels, no more (to-be 45 §2). @@ -120,14 +121,11 @@ func (c Condition) SilencedAt(now time.Time) bool { return !c.SilencedTill.IsZero() && now.Before(c.SilencedTill) } -// eventOf is the local event name of an envelope's key: `mesh-controller.condition-raised` is -// `condition-raised`. Anything not of the controller's seat is not a condition event. -func eventOf(key string) (string, bool) { return eventOfSeat(key, ControllerSeat) } - -// eventOfSeat is eventOf for the seat the take-over key says is followed: the controller's, or the keeper's (ADR 0564 clause 9). -func eventOfSeat(key, seat string) (string, bool) { +// eventOf is the local event name of an envelope's key: `mesh-conditions.condition-raised` is +// `condition-raised`. Anything not of the keeper's seat is not a condition event. +func eventOf(key string) (string, bool) { emitter, event, ok := strings.Cut(key, ".") - if !ok || emitter != seat { + if !ok || emitter != ConditionSeat { return "", false } switch event { diff --git a/modules/messenger/cmd/messenger/condition_test.go b/modules/messenger/cmd/messenger/condition_test.go index f5a9e485..2a69c59e 100644 --- a/modules/messenger/cmd/messenger/condition_test.go +++ b/modules/messenger/cmd/messenger/condition_test.go @@ -6,19 +6,19 @@ import ( "time" ) -// The contract with the controller's condition events (to-be 45 §2), as condition.go states it. +// The contract with the condition keeper's condition events (to-be 45 §2), as condition.go states it. -func TestTheEventsAreTheControllersSeat(t *testing.T) { +func TestTheEventsAreTheKeepersSeat(t *testing.T) { for key, want := range map[string]string{ - "mesh-controller.condition-raised": EventRaised, - "mesh-controller.condition-changed": EventChanged, - "mesh-controller.condition-cleared": EventCleared, + "mesh-conditions.condition-raised": EventRaised, + "mesh-conditions.condition-changed": EventChanged, + "mesh-conditions.condition-cleared": EventCleared, } { if got, ok := eventOf(key); !ok || got != want { t.Errorf("%s: %q %v", key, got, ok) } } - for _, key := range []string{"mesh-controller.applied", "gitea.condition-raised", "condition-raised"} { + for _, key := range []string{"mesh-controller.condition-raised", "mesh-conditions.applied", "gitea.condition-raised", "condition-raised"} { if _, ok := eventOf(key); ok { t.Errorf("%s read as a condition event", key) } diff --git a/modules/messenger/cmd/messenger/history_test.go b/modules/messenger/cmd/messenger/history_test.go index a8285d77..2e4dc210 100644 --- a/modules/messenger/cmd/messenger/history_test.go +++ b/modules/messenger/cmd/messenger/history_test.go @@ -86,7 +86,7 @@ func openAt(t *testing.T, backlog []backlogEvent, at time.Time) []Condition { func replay(t *testing.T, h *Holder, backlog []backlogEvent, skip func(Condition, string) bool) { t.Helper() for _, e := range backlog { - event, ok := eventOf(ControllerSeat + "." + e.event) + event, ok := eventOf(ConditionSeat + "." + e.event) if !ok { t.Fatalf("%s is not a condition event", e.event) } diff --git a/modules/messenger/cmd/messenger/main.go b/modules/messenger/cmd/messenger/main.go index 2cc92c95..f237289a 100644 --- a/modules/messenger/cmd/messenger/main.go +++ b/modules/messenger/cmd/messenger/main.go @@ -18,7 +18,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/health" ) func errorf(format string, a ...any) error { return fmt.Errorf(format, a...) } @@ -413,12 +412,11 @@ func run(h *Holder, l *listening) { logf("[messenger] %s cannot send: %v", ch.Name(), err) } } - // The state first: what is open now is read from the controller before any event is taken, so + // The state first: what is open now is read from the keeper before any event is taken, so // what the stream replays is history (novox/hq issue 271). Not reachable yet, it is asked for // again in the loop below, and old events meanwhile are kept as state, never said. - follow := newTakeover() h.Start() - syncFromController(h, follow) + syncFromKeeper(h) go func() { tick := time.NewTicker(5 * time.Second) defer tick.Stop() @@ -431,11 +429,10 @@ func run(h *Holder, l *listening) { } if h.WantsSync() && now.Sub(lastAsk) >= time.Minute { lastAsk = now - syncFromController(h, follow) + syncFromKeeper(h) } } }() - go followTakeover(follow, h) handle := func(e stdio.Envelope) error { if event, ok := strings.CutPrefix(e.Key, ViewModule+"."); ok { // A module in motion on a node, settled or stuck: a walk's start or end now that plans are gone (ADR 0308 §1, ADR 0539 §17). @@ -449,9 +446,8 @@ func run(h *Holder, l *listening) { } return nil } - // Only the seat the take-over key names is heard: the other's events are dropped, so the keeper's conditions - // are never held a second time beside the controller's (ADR 0564 clause 9). - event, ok := eventOfSeat(e.Key, follow.seat()) + // Only the keeper's condition events are heard (ADR 0564 clause 9; the controller's are no longer consumed). + event, ok := eventOf(e.Key) if !ok { return nil } @@ -467,7 +463,7 @@ func run(h *Holder, l *listening) { go takeAsks(h) go answerProofs(h) go followKinds(h) - // The delivery view's events say what is in motion (ADR 0539 §17), heard beside the controller's condition events. + // The delivery view's events say what is in motion (ADR 0539 §17), heard beside the keeper's condition events. go func() { for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { if err := stdio.Subscribe(ViewModule+".*", handle); err == nil { @@ -480,13 +476,10 @@ func run(h *Holder, l *listening) { } }() for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { - err := stdio.Subscribe(ControllerSeat+".*", handle) - if err == nil { - err = stdio.Subscribe(health.KeeperModule+".*", handle) - } + err := stdio.Subscribe(ConditionSeat+".*", handle) if err == nil { l.set("listening") - logf("[messenger] listening for the controller's condition events") + logf("[messenger] listening for the condition keeper's condition events") return } l.set("not yet: " + err.Error()) @@ -632,8 +625,8 @@ func followKinds(h *Holder) { } } -// syncFromController reads the controller's open conditions and gives them to the holder. -func syncFromController(h *Holder, follow *takeover) { +// syncFromKeeper reads the condition keeper's open conditions and gives them to the holder. +func syncFromKeeper(h *Holder) { asked := h.Now() type answer struct { raw json.RawMessage @@ -641,7 +634,7 @@ func syncFromController(h *Holder, follow *takeover) { } done := make(chan answer, 1) go func() { - raw, err := stdio.Ask("seat:"+follow.seat()+".conditions", map[string]any{}) + raw, err := stdio.Ask("seat:"+ConditionSeat+".conditions", map[string]any{}) done <- answer{raw, err} }() var a answer diff --git a/modules/messenger/cmd/messenger/manifest_test.go b/modules/messenger/cmd/messenger/manifest_test.go index 6e61b0de..20e660c1 100644 --- a/modules/messenger/cmd/messenger/manifest_test.go +++ b/modules/messenger/cmd/messenger/manifest_test.go @@ -80,14 +80,21 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { // t.Fatalf("uses %v", m.Uses) } if !reflect.DeepEqual(m.Consumes, []string{ - ControllerSeat + "." + EventRaised, ControllerSeat + "." + EventChanged, ControllerSeat + "." + EventCleared, "mesh-conditions." + EventRaised, "mesh-conditions." + EventChanged, "mesh-conditions." + EventCleared, ViewModule + "." + EventMoving, ViewModule + "." + EventSettled, ViewModule + "." + EventStuck, "intake.choice", "intake.link", "intake.failed", "intake.standing"}) { t.Fatalf("consumes %v", m.Consumes) } + // It consumes no mesh-controller event and invokes no mesh-controller verb (774/S12/AC2). + for _, list := range [][]string{m.Consumes, m.Invokes} { + for _, x := range list { + if strings.Contains(x, "mesh-controller") { + t.Errorf("%s names the controller", x) + } + } + } // It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues // track which conditions. - if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions", + if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{ "seat:mesh-conditions.conditions", "seat:" + EngineSeat + ".root-free", TrackingVerb}) { t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes) } diff --git a/modules/messenger/cmd/messenger/preview.go b/modules/messenger/cmd/messenger/preview.go index dc73f910..dcdeb523 100644 --- a/modules/messenger/cmd/messenger/preview.go +++ b/modules/messenger/cmd/messenger/preview.go @@ -35,7 +35,7 @@ var previewable = []string{"raised", "reminder", "quiet-reminder", "escalated", func (h *Holder) Preview(key, what string) (map[string]any, error) { key = strings.TrimSpace(key) if key == "" { - return nil, errorf("key is required: the condition's key, as the controller names it — `mesh-controller.conditions` says which are open") + return nil, errorf("key is required: the condition's key, as the keeper names it — `mesh-conditions.conditions` says which are open") } what = strings.TrimSpace(what) if what == "" { @@ -75,7 +75,7 @@ func (h *Holder) Preview(key, what string) (map[string]any, error) { return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds the conditions %s", key, r.Origin, strings.Join(conditions, ", ")) case len(conditions) == 0: - return nil, errorf("no condition %s is open here, and this router holds none: `mesh-controller.conditions` says what the controller holds open", key) + return nil, errorf("no condition %s is open here, and this router holds none: `mesh-conditions.conditions` says what the keeper holds open", key) default: return nil, errorf("no condition %s is open here; it holds %s", key, strings.Join(conditions, ", ")) } diff --git a/modules/messenger/cmd/messenger/preview_test.go b/modules/messenger/cmd/messenger/preview_test.go index ec23f9f7..33cdb95b 100644 --- a/modules/messenger/cmd/messenger/preview_test.go +++ b/modules/messenger/cmd/messenger/preview_test.go @@ -195,8 +195,9 @@ func TestAPreviewOfWhatThisRouterDoesNotHoldIsRefusedByName(t *testing.T) { t.Errorf("the refusal says neither what was asked nor what is held: %v", err) } - if _, err := h.Preview("", ""); err == nil || !strings.Contains(err.Error(), "key is required") { - t.Errorf("no key: %v", err) + if _, err := h.Preview("", ""); err == nil || !strings.Contains(err.Error(), "key is required") || + !strings.Contains(err.Error(), "mesh-conditions.conditions") || strings.Contains(err.Error(), "mesh-controller") { + t.Errorf("no key: %v (it says which conditions are open by the keeper's verb, never the controller's; 774/S12/AC1)", err) } if _, err := h.Preview(k.Key, "shouted"); err == nil || !strings.Contains(err.Error(), "shouted") { t.Errorf("an unknown message: %v", err) diff --git a/modules/messenger/cmd/messenger/sync.go b/modules/messenger/cmd/messenger/sync.go index 34d1d696..efeaa6ee 100644 --- a/modules/messenger/cmd/messenger/sync.go +++ b/modules/messenger/cmd/messenger/sync.go @@ -180,11 +180,3 @@ func (h *Holder) WantsSync() bool { h.init() return h.wantSync || h.syncedAt.IsZero() || h.Now().Sub(h.syncedAt) >= ResyncEvery } - -// AskSync has the next pass of the loop read the open conditions again (the take-over key named another seat). -func (h *Holder) AskSync() { - h.mu.Lock() - h.init() - h.wantSync = true - h.mu.Unlock() -} diff --git a/modules/messenger/cmd/messenger/takeover.go b/modules/messenger/cmd/messenger/takeover.go deleted file mode 100644 index e02ae421..00000000 --- a/modules/messenger/cmd/messenger/takeover.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "sync" - "time" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/health" -) - -// takeover is which seat's condition events the messenger hears: the controller's until the keeper's take-over key -// says otherwise (novox/hq ADR 0564 clause 9, issue 1952 R1). An absent key follows the controller. -type takeover struct { - mu sync.Mutex - t health.Takeover -} - -func newTakeover() *takeover { - return &takeover{t: health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController}} -} - -func (f *takeover) seat() string { f.mu.Lock(); defer f.mu.Unlock(); return f.t.Seat() } - -// set takes the key as a watch handed it and says whether the followed seat changed. -func (f *takeover) set(value []byte, found bool) (changed bool, err error) { - t, err := health.DecodeTakeover(value, found) - if err != nil { - return false, err - } - f.mu.Lock() - defer f.mu.Unlock() - changed = f.t.Seat() != t.Seat() - f.t = t - return changed, nil -} - -// followTakeover watches the take-over key for the life of the process, and asks the holder to read the followed -// seat's open conditions again when it changes. A key it cannot read leaves the seat it followed. -func followTakeover(f *takeover, h *Holder) { - for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { - err := stdio.State(health.KeeperModule+"."+health.TakeoverState).Watch(health.TakeoverKey, func(c stdio.StateChange) error { - changed, err := f.set(c.Value, c.Op != "delete" && len(c.Value) > 0) - if err != nil { - logf("[messenger] the take-over key is unreadable (%v); still hearing %s", err, f.seat()) - return nil - } - if changed { - logf("[messenger] the take-over key now names %s", f.seat()) - h.AskSync() - } - return nil - }) - if err == nil { - return - } - logf("[messenger] not watching the take-over key yet (%v); hearing %s", err, f.seat()) - time.Sleep(wait) - } -} diff --git a/modules/messenger/cmd/messenger/words_test.go b/modules/messenger/cmd/messenger/words_test.go index 5403bdf4..5322fe02 100644 --- a/modules/messenger/cmd/messenger/words_test.go +++ b/modules/messenger/cmd/messenger/words_test.go @@ -98,7 +98,7 @@ var examples = []example{{ explanation: "Needs you: mend or remove them on shanks, or silence this if they do not matter. On shanks, " + "mnt-recalbox (a mount), storage-media (a mount) and greenclip failed. No module manages them, so the mesh " + "does not repair them.", - actions: []Action{{Label: "Silence for a week", Verb: "mesh-controller.conditions", + actions: []Action{{Label: "Silence for a week", Verb: "mesh-conditions.conditions", Arguments: map[string]string{"silence": "machine.shanks.units", "for": "7d", "why": "", "cause": "operator-answer"}}}, resolved: "No failed services on shanks any more", popup: true, title: "3 failed services on shanks", diff --git a/modules/messenger/module.json b/modules/messenger/module.json index eec860f0..40221f1b 100644 --- a/modules/messenger/module.json +++ b/modules/messenger/module.json @@ -74,9 +74,6 @@ "channel" ], "consumes": [ - "mesh-controller.condition-raised", - "mesh-controller.condition-changed", - "mesh-controller.condition-cleared", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", @@ -92,14 +89,10 @@ "refused" ], "invokes": [ - "seat:mesh-controller.conditions", "seat:mesh-conditions.conditions", "seat:node-engine.root-free", "seat:issue-tracker.tracking" ], - "reads": [ - "mesh-conditions.takeover" - ], "state": [ "open", "sent",