From 45902cd728496e547aada4d4d02d6557c52c82a0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:14:50 +0100 Subject: [PATCH] minio: read who holds a seat from the placement seat's state, not the controller's seats (issue 1955, ADR 0571 clause 4) --- .../minio/cmd/object-store/manifest_test.go | 8 +- modules/minio/cmd/object-store/server.go | 82 ++++++++----------- modules/minio/cmd/object-store/server_test.go | 18 ++-- modules/minio/go.mod | 2 +- modules/minio/go.sum | 16 +++- modules/minio/module.json | 4 +- 6 files changed, 63 insertions(+), 67 deletions(-) diff --git a/modules/minio/cmd/object-store/manifest_test.go b/modules/minio/cmd/object-store/manifest_test.go index 34e5ebb9..58494c7f 100644 --- a/modules/minio/cmd/object-store/manifest_test.go +++ b/modules/minio/cmd/object-store/manifest_test.go @@ -20,7 +20,7 @@ func manifest(t *testing.T) map[string]any { return m } -// 1516/AC6: minio claims object-store (scope mesh) serving link, invokes seat:mesh-controller.seats, builds the Go +// 1516/AC6: minio claims object-store (scope mesh) serving link, reads the placement seat's seat-holders, builds the Go // bundle, and no tool named minio_presigned_url remains. func TestManifestClaimsTheSeat(t *testing.T) { m := manifest(t) @@ -38,9 +38,9 @@ func TestManifestClaimsTheSeat(t *testing.T) { if c["name"] != "object-store" || c["scope"] != "mesh" || len(serves) != 1 || serves[0] != "link" { t.Fatalf("claim: %v", c) } - inv := m["invokes"].([]any) - if len(inv) != 1 || inv[0] != "seat:mesh-controller.seats" { - t.Fatalf("invokes: %v", inv) + reads, _ := m["reads"].([]any) + if _, invokes := m["invokes"]; invokes || len(reads) != 1 || reads[0] != "mesh-delivery-assigner.seat-holders" { + t.Fatalf("reads %v and invokes %v: it reads the placement seat's holders and calls nothing", reads, m["invokes"]) } var goBundle map[string]any for _, a := range m["build"].(map[string]any)["artifacts"].([]any) { diff --git a/modules/minio/cmd/object-store/server.go b/modules/minio/cmd/object-store/server.go index 9b4b59e5..789873b8 100644 --- a/modules/minio/cmd/object-store/server.go +++ b/modules/minio/cmd/object-store/server.go @@ -13,6 +13,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/objectlink" ) @@ -59,11 +60,11 @@ func linkKeyID(bucket string) string { type server struct { configPath, linksDir, receives string now func() time.Time - // seats asks the controller for its seats; a test replaces it. - seats func() (json.RawMessage, error) + // seats reads the placement seat's state of who holds which seat, every key; a test replaces it. + seats func() (map[string][]byte, error) mu sync.Mutex - holders map[string]bool // "/." + keys map[string][]byte // seat-holders, as read readAt time.Time haveSet bool } @@ -77,8 +78,23 @@ func newServer(config, links, receives string) (*server, error) { return &server{configPath: config, linksDir: links, receives: receives, now: time.Now, seats: askSeats}, nil } -func askSeats() (json.RawMessage, error) { - return stdio.Ask("seat:mesh-controller.seats", map[string]any{}) +func askSeats() (map[string][]byte, error) { + state := stdio.State(delivery.SeatHoldersRead) + names, err := state.Keys() + if err != nil { + return nil, err + } + keys := map[string][]byte{} + for _, k := range names { + e, err := state.Get(k) + if err != nil { + return nil, err + } + if e != nil { + keys[k] = e.Value + } + } + return keys, nil } // link answers one ask. @@ -206,7 +222,7 @@ func (s *server) allowed(g grant, node, module, method string) error { if seat, isSeat := strings.CutPrefix(sh.To, "seat:"); isSeat { held, err := s.holds(seat, node, module) if err != nil { - return false, fmt.Errorf("a share names seat %s and the controller's seats cannot be read to tell whether this caller holds it: %w", seat, err) + return false, fmt.Errorf("a share names seat %s and the placement seat's holders cannot be read to tell whether this caller holds it: %w", seat, err) } return held, nil } @@ -249,58 +265,26 @@ func has(list []string, v string) bool { return false } -// holds says whether node.module holds the seat, from the controller's seats, read at most once a minute. A read that -// failed is not cached: the next ask reads again. +// holds says whether node.module holds the seat, from the placement seat's state of who holds which seat (novox/hq ADR +// 0571 clause 4), read at most once a minute. A read that failed is not cached: the next ask reads again. func (s *server) holds(seat, node, module string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() if !s.haveSet || s.now().Sub(s.readAt) >= holdersFor { - raw, err := s.seats() + keys, err := s.seats() if err != nil { return false, err } - set, err := holdersIn(raw) - if err != nil { - return false, err - } - s.holders, s.readAt, s.haveSet = set, s.now(), true + s.keys, s.readAt, s.haveSet = keys, s.now(), true } - return s.holders[seat+"/"+node+"."+module], nil -} - -// holdersIn reads the controller's `seats` answer (answer.seats[].holders[] of {node, module}, and answer.outside[] of -// {Claim, Node, Module}) into "/.". The answer may arrive wrapped in {"answer": ...}. -func holdersIn(raw json.RawMessage) (map[string]bool, error) { - var doc struct { - Answer json.RawMessage `json:"answer"` - Seats []struct { - Seat string `json:"seat"` - Holders []struct { - Node string `json:"node"` - Module string `json:"module"` - } `json:"holders"` - } `json:"seats"` - Outside []struct { - Claim, Node, Module string - } `json:"outside"` + holders, err := delivery.HoldersOf(s.keys, seat, node) + if err != nil { + return false, err } - if err := json.Unmarshal(raw, &doc); err != nil { - return nil, fmt.Errorf("the controller's seats answer is not JSON: %w", err) - } - if len(doc.Answer) > 0 && string(doc.Answer) != "null" { - return holdersIn(doc.Answer) - } - if doc.Seats == nil && doc.Outside == nil { - return nil, errors.New("the controller's seats answer holds neither seats nor outside claims") - } - out := map[string]bool{} - for _, s := range doc.Seats { - for _, h := range s.Holders { - out[s.Seat+"/"+h.Node+"."+h.Module] = true + for _, h := range holders { + if h.Node == node && h.Module == module { + return true, nil } } - for _, o := range doc.Outside { - out[o.Claim+"/"+o.Node+"."+o.Module] = true - } - return out, nil + return false, nil } diff --git a/modules/minio/cmd/object-store/server_test.go b/modules/minio/cmd/object-store/server_test.go index 2f944a27..ba7bc956 100644 --- a/modules/minio/cmd/object-store/server_test.go +++ b/modules/minio/cmd/object-store/server_test.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "errors" "net/url" "os" @@ -25,12 +24,13 @@ const grantFile = `{ ] }` -// The seats answer is the controller's (mesh-controller.seats read on 2026-10-21): seats[].holders[] of {node, module}, -// and outside[] of {Claim, Node, Module}. -const seatsAnswer = `{"answer": {"seats": [ - {"seat": "node-files", "scope": "node", "holders": [{"node": "g14", "module": "mesh-files"}, {"node": "shanks", "module": "mesh-files"}]}, - {"seat": "node-backup", "scope": "node", "holders": [{"node": "g14", "module": "restic"}]} - ], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "shanks", "Module": "brother", "Site": "home", "Kind": ""}]}}` +// The placement seat's state of who holds which seat (seat-holders): a key per node seat and node, a key per mesh seat. +var seatHolderKeys = map[string][]byte{ + "node-files.g14": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"g14","holders":[{"module":"mesh-files","node":"g14"}]}`), + "node-files.shanks": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"shanks","holders":[{"module":"mesh-files","node":"shanks"}]}`), + "node-backup.g14": []byte(`{"v":1,"seat":"node-backup","scope":"node","node":"g14","holders":[{"module":"restic","node":"g14"}]}`), + "printer": []byte(`{"v":1,"seat":"printer","scope":"mesh","holders":[{"module":"brother","node":"shanks"}]}`), +} type rig struct { s *server @@ -64,12 +64,12 @@ func newRig(t *testing.T) *rig { t.Fatal(err) } s.now = func() time.Time { return r.now } - s.seats = func() (json.RawMessage, error) { + s.seats = func() (map[string][]byte, error) { r.seatsCalls++ if r.seatsErr != nil { return nil, r.seatsErr } - return json.RawMessage(seatsAnswer), nil + return seatHolderKeys, nil } r.s = s return r diff --git a/modules/minio/go.mod b/modules/minio/go.mod index c09ccfda..4cf91848 100644 --- a/modules/minio/go.mod +++ b/modules/minio/go.mod @@ -2,4 +2,4 @@ module minio go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 diff --git a/modules/minio/go.sum b/modules/minio/go.sum index 9728ce93..05839926 100644 --- a/modules/minio/go.sum +++ b/modules/minio/go.sum @@ -1,2 +1,14 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f h1:7J/9jOfSfjfYpf8MOj96HwQRViQUkBNMb71RHP2mjn8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f/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= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/modules/minio/module.json b/modules/minio/module.json index 9136cd7d..60b40a4b 100644 --- a/modules/minio/module.json +++ b/modules/minio/module.json @@ -39,8 +39,8 @@ ] } ], - "invokes": [ - "seat:mesh-controller.seats" + "reads": [ + "mesh-delivery-assigner.seat-holders" ], "contributes": { "route": {