From 5235bb2e79c3e18cace2fbd992b16085bf0cd723 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 22:24:00 +0100 Subject: [PATCH] Assigner and grants part: record hand acts and warrants through the seat mesh-hand-acts, the controller's log only while that seat has no holder and the controller holds its lease (issue 1953 D8) Pins mesh-sdk 2d2aa7b in the L5 modules. --- .../cmd/mesh-delivery-assigner/main.go | 6 +-- .../cmd/mesh-delivery-assigner/pass_test.go | 4 +- .../cmd/mesh-delivery-assigner/puts.go | 16 +++++--- .../cmd/mesh-delivery-assigner/seat.go | 25 +++++++++---- .../cmd/mesh-delivery-assigner/seat_tools.go | 3 -- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 +- .../mesh-delivery-assigner/manifest_test.go | 3 +- modules/mesh-delivery-assigner/module.json | 2 + modules/mesh-grants/go.mod | 2 +- modules/mesh-grants/go.sum | 4 +- modules/mesh-grants/grants_test.go | 9 ++++- modules/mesh-grants/internal/grants/inputs.go | 2 +- .../mesh-grants/internal/grants/takeover.go | 37 +++++++++++++++++++ modules/mesh-grants/module.json | 4 +- 15 files changed, 90 insertions(+), 33 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index 996d423..23a76f4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -387,7 +387,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, return } if bus != nil { - p.acts = newPutActs(bus, wake, logger.Printf) + p.acts = newPutActs(bus, wake, logger.Printf, w.handedAway) } logger.Printf("the bucket %s is open: puts are taken further", delivery.AssignerPutsBucket) } @@ -450,10 +450,10 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, p := &passer{kvs: kvs, w: w, logf: logger.Printf, say: say, said: map[string]bool{}, answeredAt: map[string]uint64{}} p.asks = newComposeAsks(ctx, buildrun.Asker{Publisher: buildrun.JetStreamAsks{JS: js}, Waiter: buildrun.JetStreamAsks{JS: js}}, wake, logger.Printf) if bus != nil && kvs[delivery.AssignerPutsBucket] != nil { - p.acts = newPutActs(bus, wake, logger.Printf) + p.acts = newPutActs(bus, wake, logger.Printf, w.handedAway) } if bus != nil { - p.seat = newSeatActs(bus, wake, logger.Printf) + p.seat = newSeatActs(bus, wake, logger.Printf, w.handedAway) } if js != nil { p.publish = streamPublisher(ctx, js, thisNode) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 35dc5af..6b7f420 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -187,7 +187,7 @@ func putOfTheBus(t *testing.T) { p := newPasser(kvs, now) bus := &fakeBus{calls: make(chan string, 8)} woke := make(chan struct{}, 8) // each act's goroutine wakes a pass once it is done - p.acts = newPutActs(bus, func() { woke <- struct{}{} }, func(string, ...any) {}) + p.acts = newPutActs(bus, func() { woke <- struct{}{} }, func(string, ...any) {}, nil) p.acts.snapshot = func(context.Context, string, string) (string, error) { return "novox's restore point of nats (streams) taken 22:01", nil } @@ -288,7 +288,7 @@ func putOfTheBus(t *testing.T) { if got.Step != delivery.PutWanted || err != nil || w.FP != fp || w.Composed != digest || !w.Put || !w.Hold { t.Fatalf("snapshotted, the put stands at %s (%s); the want %+v, %v", got.Step, got.Words, w, err) } - if call := <-bus.calls; !strings.Contains(call, "warranted") || !strings.Contains(call, a.Ask.ID) { + if call := <-bus.calls; !strings.HasPrefix(call, "seat:mesh-hand-acts.warranted ") || !strings.Contains(call, a.Ask.ID) { // ADR 0565 §11: into the seat t.Errorf("the warrant was recorded as %q", call) } world(delivery.AssignerWantsBucket, "want.novox.nats", wantsKV.values["want.novox.nats"]) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go index 10e48f9..26be2fa 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -13,6 +13,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/handacts" "mesh-delivery-assigner/internal/assign" ) @@ -46,14 +47,16 @@ type putActs struct { snapped map[string]string snapNo map[string]string warranted map[string]bool + // rec records the put's warrant in the hand-act log (ADR 0565 §11). + rec handacts.Recorder warrantNo map[string]string said map[string]string // snapshot takes the bus's snapshot on node; a variable so a test takes none. snapshot func(ctx context.Context, node, module string) (string, error) } -func newPutActs(bus caller, wake func(), logf func(string, ...any)) *putActs { - a := &putActs{bus: bus, wake: wake, logf: logf, snapping: map[string]bool{}, snapped: map[string]string{}, snapNo: map[string]string{}, warranted: map[string]bool{}, warrantNo: map[string]string{}, +func newPutActs(bus caller, wake func(), logf func(string, ...any), leaseAway func() bool) *putActs { + a := &putActs{rec: handacts.Recorder{Ask: bus.Call, LeaseAway: leaseAway}, bus: bus, wake: wake, logf: logf, snapping: map[string]bool{}, snapped: map[string]string{}, snapNo: map[string]string{}, warranted: map[string]bool{}, warrantNo: map[string]string{}, said: map[string]string{}} a.snapshot = a.snapshotTheBus return a @@ -381,8 +384,9 @@ func (a *putActs) snapshotTheBus(ctx context.Context, node, module string) (stri } } -// warrant records once, in the controller's hand-act log, that the bus's want was written on the operator's warrant: the -// router's record of the teller's ask says who chose and how (ADR 0274). +// warrant records once, in the hand-act log (the seat mesh-hand-acts', or the controller's while that seat has no holder and +// the controller holds its lease, ADR 0565 §11), that the bus's want was written on the operator's warrant: the router's +// record of the teller's ask says who chose and how (ADR 0274). func (a *putActs) warrant(id, ask string) { a.mu.Lock() if a.warranted[id] { @@ -392,10 +396,10 @@ func (a *putActs) warrant(id, ask string) { a.warranted[id] = true a.mu.Unlock() go func() { - if _, err := a.bus.Call("seat:"+catalogue.ControllerSeatName+".warranted", map[string]any{"asker": delivery.TellerModule, "ask": ask}); err != nil { + if _, err := a.rec.RecordWarrant(delivery.TellerModule, ask); err != nil { a.logf("the put's warrant %s is not recorded in the hand-act log: %v", ask, err) a.mu.Lock() - a.warrantNo[id] = fmt.Sprintf("the warrant %s is not recorded in the controller's hand-act log: %v", ask, err) + a.warrantNo[id] = fmt.Sprintf("the warrant %s is not recorded in the hand-act log: %v", ask, err) a.mu.Unlock() a.wake() } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go index 42aa0bc..7ba0b4d 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go @@ -21,6 +21,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/compose/frombus" "git.novox.be/novox/mesh-sdk/go/compose/judge" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/process" "mesh-delivery-assigner/internal/assign" @@ -67,6 +68,8 @@ type seatActs struct { export map[string]exportCheck // calls is, by pair (node and module, or node and provision for a pin), what the last call for it came to. calls map[string]string + // rec records the seat's acts in the hand-act log (ADR 0565 §11). + rec handacts.Recorder } // exportCheck is the export read again for one proposal: running, its answer, or why it was not read. @@ -76,8 +79,9 @@ type exportCheck struct { err string } -func newSeatActs(bus caller, wake func(), logf func(string, ...any)) *seatActs { - return &seatActs{bus: bus, wake: wake, logf: logf, export: map[string]exportCheck{}, calls: map[string]string{}} +func newSeatActs(bus caller, wake func(), logf func(string, ...any), leaseAway func() bool) *seatActs { + return &seatActs{bus: bus, wake: wake, logf: logf, export: map[string]exportCheck{}, calls: map[string]string{}, + rec: handacts.Recorder{Ask: bus.Call, LeaseAway: leaseAway}} } // exportOf is the export read again for a proposal, asked once in the background. @@ -114,26 +118,31 @@ func (a *seatActs) forget(id string) { a.mu.Unlock() } -// record records once, in the controller's hand-act log, that placement was written on the operator's warrant (§5): the -// router's record of the teller's ask says who chose and how (ADR 0274). +// record records once, in the hand-act log, that placement was written on the operator's warrant (§5): the router's +// record of the teller's ask says who chose and how (ADR 0274). The log is the seat mesh-hand-acts' from its delivery, and +// the controller's while that seat has no holder and the controller holds its lease (ADR 0565 §11, handacts.Recorder). func (a *seatActs) record(warrant string) { if a == nil || warrant == "" { return } ask, _, _ := strings.Cut(warrant, "@") go func() { - if _, err := a.bus.Call("seat:"+catalogue.ControllerSeatName+".warranted", map[string]any{"asker": delivery.TellerModule, "ask": ask}); err != nil { + if taken, err := a.rec.RecordWarrant(delivery.TellerModule, ask); err != nil { a.logf("the placement's warrant %s is not recorded in the hand-act log: %v", ask, err) + } else { + a.logf("the placement's warrant %s is recorded in the log of %s", ask, taken.Log) } }() } -// handAct records a hand-back in the controller's hand-act log (§7). +// handAct records a hand-back in the hand-act log (§7), as record does. func (a *seatActs) handAct(prop delivery.PlacementProposal) { go func() { - if _, err := a.bus.Call(handActCall, map[string]any{"what": "mesh-placement hand-back of " + strings.Join(prop.Nodes(), ", "), - "why": prop.Why, "cause": "asked by " + prop.By + ", approved by the operator"}); err != nil { + if taken, err := a.rec.Record(handacts.Request{What: "mesh-placement hand-back of " + strings.Join(prop.Nodes(), ", "), + Why: prop.Why, Cause: "asked by " + prop.By + ", approved by the operator"}); err != nil { a.logf("the hand-back %s is not recorded in the hand-act log: %v", prop.ID, err) + } else { + a.logf("the hand-back %s is recorded in the log of %s", prop.ID, taken.Log) } }() } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go index ebbf4fb..3c683f7 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go @@ -318,9 +318,6 @@ func (s *seatTools) leased() bool { return away } -// handActCall records a hand act in the controller's hand-act log. -const handActCall = "seat:" + catalogue.ControllerSeatName + ".hand-act" - // doHandBack is the verb hand-back (§7): the operator's alone, so it is asked as a proposal is. It writes a hand-back // proposal naming every key at its revision; the assigner raises its authorising ask and, on its warrant, deletes the keys, // records the hand act and says placement-changed; every node returns to the controller's compositions. diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 97fcb8a..e1d9aad 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index ae67790..685613a 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba h1:uXwkcQWgbu/UthizGHFaKPiA4NBVJgqL9q2jJnnGR1o= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029204453-69fcb6c1c1ba/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd h1:WhqMEXFZ9Zc+fEfLsnf1jhhGkc4IqMwUpXXwZq+V3+E= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 3365117..3964db8 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -79,7 +79,8 @@ func TestManifest(t *testing.T) { } if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.hand-act", "seat:mesh-controller.pin", "seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin", - "seat:mesh-controller.warranted", "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { + "seat:mesh-controller.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.warranted", // ADR 0565 §11: the seat, the controller's only while it has no holder + "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } if !slices.Equal(m.Emits, []string{"put"}) { diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 28aaf55..267eb62 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -47,6 +47,8 @@ "seat:mesh-controller.unassign", "seat:mesh-controller.unpin", "seat:mesh-controller.warranted", + "seat:mesh-hand-acts.hand-act", + "seat:mesh-hand-acts.warranted", "seat:node-backup.backed-up", "seat:node-backup.now" ], diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 5676e43..361c8a5 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211750-2e5cc4f16635 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index fe45f51..e8df9ed 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211750-2e5cc4f16635 h1:ImOyj4WqH9lwxOr95aH9Wq2iJkUwdtnEyPrASm9TrYA= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211750-2e5cc4f16635/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd h1:WhqMEXFZ9Zc+fEfLsnf1jhhGkc4IqMwUpXXwZq+V3+E= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029212112-2d2aa7ba9bbd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 891abee..53b8e9b 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -1388,9 +1388,14 @@ func TestGrants(t *testing.T) { row.between() } said = nil + calls := len(b.calls) if err := p.Warranted(row.option, warrant(id)); err != nil { t.Fatal(err) } + // 1953/D8 (ADR 0565 §11): a switch made is recorded in the hand-act log's seat; one refused or declined is not. + if recorded := slices.Contains(b.calls[calls:], grants.CallRecordWarrant); recorded != (row.outcome == "recorded") { + t.Errorf("recorded in the hand-act log %v, the act %s", recorded, row.outcome) + } l := last() if l.Said != row.outcome || l.Ask != id || p.SwitchState() != row.state { t.Errorf("the act = %+v, the switch %s; want %s and %s", l, p.SwitchState(), row.outcome, row.state) @@ -1642,8 +1647,8 @@ func TestGrants(t *testing.T) { } } for _, c := range b.calls { - if c != grants.CallUsers && c != grants.CallStreams { - t.Errorf("called %s: the part calls only the bus module's read tools", c) + if c != grants.CallUsers && c != grants.CallStreams && c != grants.CallRecordWarrant { + t.Errorf("called %s: the part calls only the bus module's read tools, and the hand-act log for a switch", c) } } for _, e := range b.events { diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index e0f702d..556ff70 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -52,7 +52,7 @@ const ( ) // Invokes is every tool the part calls. -var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree} +var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant, CallRecordWarrantController} // Reader reads another part's state, named `.`. A refusal (a read not granted yet, a bucket not there) is // an error the part names as a gap and composes past: it never stops for one. diff --git a/modules/mesh-grants/internal/grants/takeover.go b/modules/mesh-grants/internal/grants/takeover.go index bd5e678..d5c94f7 100644 --- a/modules/mesh-grants/internal/grants/takeover.go +++ b/modules/mesh-grants/internal/grants/takeover.go @@ -1,6 +1,8 @@ package grants import ( + "git.novox.be/novox/mesh-sdk/go/handacts" + "crypto/rand" "encoding/hex" "encoding/json" @@ -378,6 +380,7 @@ func (p *Part) Warranted(o asks.Option, w asks.Warrant) error { if err := p.finish(s, a); err != nil { return err } + p.recordWarrant(w.Ask) // The bus node turns on the key: written now, so the switch reaches it on the operator's yes (ADR 0547 clause 8). if err := p.syncFollow(s.State); err != nil { return fmt.Errorf("the switch is recorded and the key the bus node follows could not be written (the next pass writes it): %w", err) @@ -512,3 +515,37 @@ func (p *Part) TakeOverState() (any, error) { } return out, nil } + +// Module is the grants part's module, the asker of its take-over and hand-back. +const Module = "mesh-grants" + +// The hand-act log's verbs the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's, and the +// controller's while the seat has no holder and the controller holds its lease (handacts.Recorder). +const ( + CallRecordWarrant = "seat:" + handacts.Seat + ".warranted" + CallRecordWarrantController = "seat:mesh-controller.warranted" +) + +// recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask. +// Said, never fatal: the switch stands either way. +func (p *Part) recordWarrant(ask string) { + if p.Call == nil { + return + } + rec := handacts.Recorder{Ask: p.Call.Call, LeaseAway: func() bool { + if p.Read == nil { + return false + } + away, _ := leaseHandedAway(p.Read) + return away + }} + taken, err := rec.RecordWarrant(Module, ask) + if p.Logf == nil { + return + } + if err != nil { + p.Logf("the switch on the warrant %s is not recorded in the hand-act log: %v", ask, err) + return + } + p.Logf("the switch on the warrant %s is recorded in the log of %s", ask, taken.Log) +} diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index 2294408..2d25d3a 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -30,7 +30,9 @@ "nats.nats_streams", "nats.nats_user_can", "nats.nats_users", - "seat:mesh-controller.root-free" + "seat:mesh-controller.root-free", + "seat:mesh-hand-acts.warranted", + "seat:mesh-controller.warranted" ], "emits": [ "asserted",