From 3d3524dd69635441ee1d42362d26ea006b71df9b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:12:29 +0100 Subject: [PATCH] mesh-watcher: read who holds the controller and the bus from the placement seat's state, not the controller's seats (issue 1955, ADR 0571 clause 4) --- modules/mesh-watcher/cmd/mesh-watcher/main.go | 73 ++++++++----------- .../cmd/mesh-watcher/manifest_test.go | 2 +- .../cmd/mesh-watcher/watcher_test.go | 23 +++--- modules/mesh-watcher/go.mod | 2 +- modules/mesh-watcher/go.sum | 4 +- modules/mesh-watcher/module.json | 10 ++- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/modules/mesh-watcher/cmd/mesh-watcher/main.go b/modules/mesh-watcher/cmd/mesh-watcher/main.go index 22914015..a4fb0b55 100644 --- a/modules/mesh-watcher/cmd/mesh-watcher/main.go +++ b/modules/mesh-watcher/cmd/mesh-watcher/main.go @@ -15,6 +15,7 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -150,60 +151,46 @@ func roundTrip(node string) error { } } -// controlNodes reads which machines hold the controller and the bus. +// controlNodes reads which machines hold the controller and the bus, from the placement seat's state of who holds +// which seat (novox/hq ADR 0571 clause 4). func controlNodes() ([]string, error) { - raw, err := stdio.Ask("seat:mesh-controller.seats", map[string]any{}) + st := stdio.State(delivery.SeatHoldersRead) + names, err := st.Keys() if err != nil { return nil, err } - return holdersOf(raw, "mesh-controller", "mesh-broker") + keys := map[string][]byte{} + for _, k := range names { + e, err := st.Get(k) + if err != nil { + return nil, err + } + if e != nil { + keys[k] = e.Value + } + } + return holderNodes(keys, "mesh-controller", "mesh-broker") } -// holdersOf reads the seats verb's answer, bare or inside a tool reply, for the named seats' machines. -func holdersOf(raw json.RawMessage, seats ...string) ([]string, error) { - var answer struct { - Seats []struct { - Seat string `json:"seat"` - Holders []struct { - Node string `json:"node"` - } `json:"holders"` - } `json:"seats"` - Output string `json:"output"` - Content []struct { - Text string `json:"text"` - } `json:"content"` - } - if err := json.Unmarshal(raw, &answer); err != nil { - var s string - if json.Unmarshal(raw, &s) == nil { - return holdersOf(json.RawMessage(s), seats...) - } - return nil, fmt.Errorf("the seats answer is not JSON") - } - if len(answer.Seats) == 0 { - switch { - case answer.Output != "": - return holdersOf(json.RawMessage(answer.Output), seats...) - case len(answer.Content) > 0: - return holdersOf(json.RawMessage(answer.Content[0].Text), seats...) - } - return nil, fmt.Errorf("the seats answer lists no seat") - } +// holderNodes is the machines holding any of the seats, in the order the seats are named, each once. +func holderNodes(keys map[string][]byte, seats ...string) ([]string, error) { var out []string seen := map[string]bool{} - for _, s := range answer.Seats { - for _, want := range seats { - if s.Seat != want { - continue - } - for _, h := range s.Holders { - if !seen[h.Node] { - seen[h.Node] = true - out = append(out, h.Node) - } + for _, seat := range seats { + holders, err := delivery.HoldersOf(keys, seat, "") + if err != nil { + return nil, err + } + for _, h := range holders { + if !seen[h.Node] { + seen[h.Node] = true + out = append(out, h.Node) } } } + if len(out) == 0 { + return nil, fmt.Errorf("the placement seat's state names no holder of %s", strings.Join(seats, " or ")) + } return out, nil } diff --git a/modules/mesh-watcher/cmd/mesh-watcher/manifest_test.go b/modules/mesh-watcher/cmd/mesh-watcher/manifest_test.go index ee7f1c9e..ddefa504 100644 --- a/modules/mesh-watcher/cmd/mesh-watcher/manifest_test.go +++ b/modules/mesh-watcher/cmd/mesh-watcher/manifest_test.go @@ -37,7 +37,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { if !reflect.DeepEqual(m.Consumes, []string{ControllerSeat + "." + HeartbeatEvent, "mesh-self-check.checked"}) { t.Fatalf("consumes %v", m.Consumes) } - if !reflect.DeepEqual(m.Invokes, []string{m.Module + ".watcher_ping", "seat:mesh-controller.seats"}) { + if !reflect.DeepEqual(m.Invokes, []string{m.Module + ".watcher_ping"}) { t.Fatalf("invokes %v", m.Invokes) } if len(m.Claims) != 0 { diff --git a/modules/mesh-watcher/cmd/mesh-watcher/watcher_test.go b/modules/mesh-watcher/cmd/mesh-watcher/watcher_test.go index a335af11..bf77ce46 100644 --- a/modules/mesh-watcher/cmd/mesh-watcher/watcher_test.go +++ b/modules/mesh-watcher/cmd/mesh-watcher/watcher_test.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "errors" "strings" "testing" @@ -214,16 +213,18 @@ func TestOnTheControlNodeItSaysItIsMisplaced(t *testing.T) { } } -func TestTheSeatsAnswerIsReadInEitherShape(t *testing.T) { - bare := `{"seats":[{"seat":"mesh-controller","holders":[{"node":"n1","module":"mesh-controller"}]}, - {"seat":"mesh-broker","holders":[{"node":"n1"},{"node":"n2"}]},{"seat":"git","holders":[{"node":"n3"}]}]}` - wrapped, _ := json.Marshal(map[string]any{"content": []map[string]string{{"type": "text", "text": bare}}}) - output, _ := json.Marshal(map[string]any{"ok": true, "output": bare}) - for _, raw := range []string{bare, string(wrapped), string(output)} { - got, err := holdersOf(json.RawMessage(raw), "mesh-controller", "mesh-broker") - if err != nil || strings.Join(got, ",") != "n1,n2" { - t.Errorf("%s: %v %v", raw, got, err) - } +func TestTheHoldersAreReadFromThePlacementSeatsState(t *testing.T) { + keys := map[string][]byte{ + "mesh-controller": []byte(`{"v":1,"seat":"mesh-controller","scope":"mesh","holders":[{"module":"mesh-controller","node":"n1"}]}`), + "mesh-broker": []byte(`{"v":1,"seat":"mesh-broker","scope":"mesh","holders":[{"module":"nats","node":"n1"},{"module":"nats","node":"n2"}]}`), + "git": []byte(`{"v":1,"seat":"git","scope":"mesh","holders":[{"module":"gitea","node":"n3"}]}`), + } + got, err := holderNodes(keys, "mesh-controller", "mesh-broker") + if err != nil || strings.Join(got, ",") != "n1,n2" { + t.Errorf("%v %v", got, err) + } + if _, err := holderNodes(map[string][]byte{}, "mesh-controller", "mesh-broker"); err == nil { + t.Error("no holder named, and no error") } } diff --git a/modules/mesh-watcher/go.mod b/modules/mesh-watcher/go.mod index 168dd6c0..fae002dc 100644 --- a/modules/mesh-watcher/go.mod +++ b/modules/mesh-watcher/go.mod @@ -2,4 +2,4 @@ module mesh-watcher go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 diff --git a/modules/mesh-watcher/go.sum b/modules/mesh-watcher/go.sum index ea2abb0e..eda36e26 100644 --- a/modules/mesh-watcher/go.sum +++ b/modules/mesh-watcher/go.sum @@ -1,2 +1,2 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 h1:OsA1v6n736a7RqEV8Nkip8pf5ILMbzj0PWuKqcBOhK0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/modules/mesh-watcher/module.json b/modules/mesh-watcher/module.json index 26ed5097..ac05e105 100644 --- a/modules/mesh-watcher/module.json +++ b/modules/mesh-watcher/module.json @@ -7,11 +7,11 @@ "mesh-self-check.checked" ], "reads": [ - "mesh-conditions.takeover" + "mesh-conditions.takeover", + "mesh-delivery-assigner.seat-holders" ], "invokes": [ - "mesh-watcher.watcher_ping", - "seat:mesh-controller.seats" + "mesh-watcher.watcher_ping" ], "own-secrets": { "telegram-token": "${dir:state}/telegram-token" @@ -43,7 +43,9 @@ "type": "file", "path": "${dir:state}/settings.json", "mode": "0600", - "takes-config": ["telegram-chat-id"], + "takes-config": [ + "telegram-chat-id" + ], "merge": "json", "template": "files/settings.json" }