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 094b987..e84e562 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -30,9 +30,9 @@ import ( func main() { if os.Getenv("MESH_BROKER_FILE") == "" { - // Started by the tool runner, with no credential of its own: the seat's verbs (ADR 0546), served as every seat a module - // holds is, since a process never serves (ADR 0436). The process the manifest runs names its credential. - if err := serveSeat(); err != nil { + // Started by the tool runner, with no credential of its own: the put's tools (ADR 0546), served through the runtime, + // since a process never serves (ADR 0436). The process the manifest runs names its credential. + if err := serveTools(); err != nil { fmt.Fprintln(os.Stderr, "mesh-delivery-assigner:", err) os.Exit(1) } 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 49f8a3a..71b88a1 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 @@ -130,7 +130,7 @@ func (m *memKV) Update(_ context.Context, key string, value []byte, revision uin return uint64(len(value)), nil } -// memStore is the runtime's states for the seat's verbs, over the same memKVs a pass writes. +// memStore is the runtime's states for the put's tools, over the same memKVs a pass writes. type memStore map[string]*memKV func (s memStore) get(state, key string) ([]byte, uint64, bool, error) { @@ -167,8 +167,8 @@ func (b *fakeBus) Emit(eventType string, body any) error { return nil } -// ADR 0546, the first put (2026-10-25, want.novox.nats): the verb writes the put; a pass asks the controller's compose verb; -// once the composition and grant are there it asks the operator with a yes that approves; only on that warrant does it have +// ADR 0546, the first put (2026-10-25, want.novox.nats): the tool writes the put; a pass waits for the composition and grant +// the controller's take-in writes; once they are there it asks the operator with a yes that approves; only on that warrant does it have // the streams snapshotted, then writes the want held and records the warrant; the have running it healthy holds it; a release // gives the want back to the rules. Each step is said as the event put. func putOfTheBus(t *testing.T) { @@ -213,7 +213,7 @@ func putOfTheBus(t *testing.T) { builds.values[bkey] = built modules := mem() modules.values["nats"] = mod - s := newSeat(memStore{putsState: puts, delivery.BuilderModule + ".builds": builds, delivery.IndexerModule + ".modules": modules}) + s := newPutTools(memStore{putsState: puts, delivery.BuilderModule + ".builds": builds, delivery.IndexerModule + ".modules": modules}) s.now = func() time.Time { return now } was := answerWithin answerWithin = 0 @@ -245,13 +245,14 @@ func putOfTheBus(t *testing.T) { } return got } - if got := step(); got.Step != delivery.PutCompose { + if got := step(); got.Step != delivery.PutCompose || !strings.Contains(got.Words, "no composition") { t.Fatalf("the first pass left the put at %s: %s", got.Step, got.Words) } - if call := <-bus.calls; !strings.HasPrefix(call, "seat:mesh-controller.compose") || !strings.Contains(call, fp) { - t.Errorf("the controller was asked %q", call) + select { + case call := <-bus.calls: + t.Errorf("a put asked %q: it reads the compositions and asks nobody for one", call) + default: } - <-woke ckey, _ := delivery.ComposedKey("novox", "nats", fp) cd, _ := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: digest, Judgement: delivery.JudgedCannot, Words: "the have names no composition"}) world(delivery.ControllerComposedBucket, ckey, cd) 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 ec574a8..c5216be 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -16,12 +16,12 @@ import ( "mesh-delivery-assigner/internal/assign" ) -// The puts (novox/hq ADR 0546): the seat's verb `put` writes the key `.` of the assigner's puts, and each pass -// takes every put that has not ended one step further (assign.StepPut), doing the acts the step asks for: the controller's -// compose verb, the put's ask, the bus's snapshot, the warranted record. Each act's outcome is heard back as a change of the -// world (the composition, the grant, the answer) or, for the calls, as a wake once they answer; nothing polls but the -// snapshot's read-back of the backup holder, as the controller's bus step reads it (ADR 0235), bounded. Every step is said as -// the event mesh-delivery-assigner.put, so a caller waits on it. +// The puts (novox/hq ADR 0546): the tool `assigner_put` writes the key `.` of the assigner's puts, and each pass +// takes every put that has not ended one step further (assign.StepPut), doing the acts the step asks for: the put's ask, the +// bus's snapshot, the warranted record. Each act's outcome is heard back as a change of the world (the composition, the grant, +// the answer) or, for the calls, as a wake once they answer; nothing polls but the snapshot's read-back of the backup holder, +// as the controller's bus step reads it (ADR 0235), bounded. Every step is said as the event mesh-delivery-assigner.put, so a +// caller waits on it. // PutEvent is the event a put's every step is said as: mesh.mod.mesh-delivery-assigner.event.put. const PutEvent = "put" @@ -35,14 +35,12 @@ type caller interface { } // putActs keeps, by put (its key and when it was asked), what the acts of a put came to while this process runs: a restart -// asks the controller and the snapshot again, which compose and back up again without harm. +// takes the snapshot again, without harm. type putActs struct { mu sync.Mutex bus caller wake func() logf func(string, ...any) - composing map[string]time.Time - composeNo map[string]string snapping map[string]bool snapped map[string]string snapNo map[string]string @@ -53,8 +51,7 @@ type putActs struct { } func newPutActs(bus caller, wake func(), logf func(string, ...any)) *putActs { - a := &putActs{bus: bus, wake: wake, logf: logf, composing: map[string]time.Time{}, composeNo: map[string]string{}, - snapping: map[string]bool{}, snapped: map[string]string{}, snapNo: map[string]string{}, warranted: map[string]bool{}, + a := &putActs{bus: bus, wake: wake, logf: logf, snapping: map[string]bool{}, snapped: map[string]string{}, snapNo: map[string]string{}, warranted: map[string]bool{}, said: map[string]string{}} a.snapshot = a.snapshotTheBus return a @@ -118,12 +115,12 @@ func (p *passer) puts(ctx context.Context) { key, _ := delivery.PutKey(node, module) id := identity(key, put) world := p.putWorld(node, module, put, manifest, assigned[node]) - world.ComposeAsked, world.ComposeFailed, world.Snapshot, world.Snapshotting, world.SnapshotFailed = p.acts.state(id) + world.Snapshot, world.Snapshotting, world.SnapshotFailed = p.acts.state(id) next := assign.StepPut(world) // A held bus whose user list the rules moved on (ADR 0539 §7): the put follows the composition its want names. if w, ok := wanted[module][node]; ok && w.Put && w.FP == next.Put.FP && w.Composed != "" && w.Composed != next.Put.Composed && (next.Put.Step == delivery.PutWanted || next.Put.Step == delivery.PutHolding) { - if d, there := p.w.composed(node, module, w.FP); there && d.UserListOnly && d.Digest == w.Composed { + if d, _ := p.composedFor(node, module, w.FP); d != nil && d.UserListOnly && d.Digest == w.Composed { next.Put = next.Put.Said(world.Now, next.Put.Step, fmt.Sprintf("the bus's user list changed: the want names composition %s", shortHex(w.Composed))) next.Put.Composed = w.Composed } @@ -167,8 +164,8 @@ func (p *passer) putWorld(node, module string, put delivery.Put, manifest catalo } } } - if d, ok := p.w.composed(node, module, put.FP); ok { - w.Composed = &d + w.Composed, w.Granted = p.composedFor(node, module, put.FP) + if d := w.Composed; d != nil { policy := d.Policy if policy == "" { policy, _, _ = catalogue.DerivedUpgrade(manifest) @@ -183,13 +180,6 @@ func (p *passer) putWorld(node, module string, put delivery.Put, manifest catalo } } } - if gkey, err := delivery.ControllerGrantKey(module, put.FP); err == nil { - if raw := p.w.value(delivery.ControllerGrantsBucket, gkey); raw != nil { - if g, err := delivery.DecodeControllerGrant(raw); err == nil { - w.Granted = g.NamesNode(node, module) - } - } - } if hkey, err := delivery.HaveKey(node, module); err == nil { if raw := p.w.value(delivery.HaveBucket, hkey); raw != nil { if h, err := delivery.DecodeHave(raw); err == nil { @@ -200,11 +190,28 @@ func (p *passer) putWorld(node, module string, put delivery.Put, manifest catalo return w } +// composedFor is the composition of module for node at fp and whether the grant at fp names the node's accounts: the one +// place a put reads them (ADR 0546 clause 4). Today their writer is the controller (mesh-controller_composed and +// mesh-controller_grants); when composing and grants move out of the controller into delivery's own parts, this reads them +// from their new owner and nothing else changes. +func (p *passer) composedFor(node, module, fp string) (*delivery.ComposedDigest, bool) { + var out *delivery.ComposedDigest + if d, ok := p.w.composed(node, module, fp); ok { + out = &d + } + granted := false + if gkey, err := delivery.ControllerGrantKey(module, fp); err == nil { + if raw := p.w.value(delivery.ControllerGrantsBucket, gkey); raw != nil { + if g, err := delivery.DecodeControllerGrant(raw); err == nil { + granted = g.NamesNode(node, module) + } + } + } + return out, granted +} + // actOn does the acts a step asks for. func (p *passer) actOn(ctx context.Context, node, module, id string, w assign.PutWorld, next assign.PutNext) { - if next.Compose { - p.acts.compose(ctx, id, node, module, next.Put.FP) - } if next.Ask && w.Composed != nil { akey := next.Put.Ask if !p.w.has(delivery.AssignerAsksBucket, akey) { @@ -229,7 +236,7 @@ func samePut(a, b delivery.Put) bool { return a.Step == b.Step && a.Words == b.Words && a.Composed == b.Composed && a.Ask == b.Ask && a.Snapshot == b.Snapshot } -// writePut writes a put's next value over the one the pass read, by compare-and-set, unless the verb has put again or +// writePut writes a put's next value over the one the pass read, by compare-and-set, unless the tool has put again or // released it since; and says it as the event put. func (p *passer) writePut(ctx context.Context, key string, read, next delivery.Put) { kv := p.kvs[delivery.AssignerPutsBucket] @@ -262,55 +269,10 @@ func (p *passer) writePut(ctx context.Context, key string, read, next delivery.P } // state is what this process knows of a put's acts. -func (a *putActs) state(id string) (asked time.Time, composeNo, snapshot string, snapping bool, snapNo string) { +func (a *putActs) state(id string) (snapshot string, snapping bool, snapNo string) { a.mu.Lock() defer a.mu.Unlock() - return a.composing[id], a.composeNo[id], a.snapped[id], a.snapping[id], a.snapNo[id] -} - -// composeAnswer is what the controller's compose verb answers inside its seat's answer ({output, ok, answer}). -type composeAnswer struct { - OK bool `json:"ok"` - Output string `json:"output"` - Answer *struct { - Digest string `json:"digest"` - Granted bool `json:"granted"` - Grant string `json:"grant"` - } `json:"answer"` -} - -// compose asks the controller's compose verb once for a put. Its answer wakes a pass; the composition and the grant are -// read from their buckets, and a refusal is kept as the put's words. -func (a *putActs) compose(ctx context.Context, id, node, module, fp string) { - a.mu.Lock() - if _, asked := a.composing[id]; asked { - a.mu.Unlock() - return - } - a.composing[id] = time.Now() - a.mu.Unlock() - go func() { - defer a.wake() - raw, err := a.bus.Call("seat:"+catalogue.ControllerSeatName+".compose", map[string]any{"node": node, "module": module, "fp": fp}) - why := "" - var ans composeAnswer - switch { - case err != nil: - why = err.Error() - case json.Unmarshal(raw, &ans) != nil: - // An answer of another shape: the controller still running it after its answer window. The buckets say the rest. - case !ans.OK && strings.TrimSpace(ans.Output) != "": - why = strings.TrimSpace(ans.Output) - case ans.Answer != nil && ans.Answer.Digest != "" && !ans.Answer.Granted: - why = ans.Answer.Grant - } - if why != "" { - a.mu.Lock() - a.composeNo[id] = why - a.mu.Unlock() - a.logf("the controller's compose of %s on %s at %s: %s", module, node, fp, why) - } - }() + return a.snapped[id], a.snapping[id], a.snapNo[id] } // snap has the bus's snapshot taken once for a put, in the background. diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go similarity index 65% rename from modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go rename to modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go index 9de8ce7..016654b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go @@ -6,35 +6,36 @@ import ( "fmt" "os" "regexp" + "slices" "sort" "strings" "sync" "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) -// The assigner's seat (novox/hq ADR 0546), served through the tool runner as every seat a module holds is: a process never -// serves (ADR 0436, mesh-sdk process). The verbs write and read the puts; the assigner's process takes each put further -// (puts.go) and says every step as the event put. +// The assigner's tools (novox/hq ADR 0546), served through the tool runner from its bundle: a process never serves (ADR +// 0436, mesh-sdk process). They write and read the puts; the assigner's process takes each put further (puts.go) and says +// every step as the event put. They are the module's own tools, not a seat's verbs: a new seat needs a change of the +// controller, which takes none; they become the verbs of a seat once composing leaves the controller. // putsState is the assigner's state of puts, as the runtime names a module's own state. const putsState = "puts" -// answerWithin is how long `put` waits for the assigner's process to take the put up before it answers what stands. +// answerWithin is how long assigner_put waits for the assigner's process to take the put up before it answers what stands. var answerWithin = 20 * time.Second -// store is what the verbs need of the states: the runtime's (stdio.State), or a test's. +// store is what the tools need of the states: the runtime's (stdio.State), or a test's. type store interface { get(state, key string) ([]byte, uint64, bool, error) put(state, key string, value []byte) error keys(state string) ([]string, error) } -// seat serves the verbs over a store, and keeps the newest value of every put it heard, to wait on. -type seat struct { +// putTools serves the tools over a store, and keeps the newest value of every put it heard, to wait on. +type putTools struct { st store now func() time.Time mu sync.Mutex @@ -43,12 +44,12 @@ type seat struct { changed chan struct{} } -func newSeat(st store) *seat { - return &seat{st: st, now: time.Now, heard: map[string][]byte{}, changed: make(chan struct{})} +func newPutTools(st store) *putTools { + return &putTools{st: st, now: time.Now, heard: map[string][]byte{}, changed: make(chan struct{})} } // hear takes a put's change in from the watch. -func (s *seat) hear(key string, value []byte) { +func (s *putTools) hear(key string, value []byte) { s.mu.Lock() s.heard[key] = value close(s.changed) @@ -64,7 +65,7 @@ var ( // version reads the version a put names: newest (the indexer's fingerprint at the trunk), a whole fingerprint, or the start // of one the builder built, which must name one build alone. -func (s *seat) version(module, version string) (string, error) { +func (s *putTools) version(module, version string) (string, error) { v := strings.ToLower(strings.TrimSpace(version)) switch { case v == "newest": @@ -110,7 +111,7 @@ func (s *seat) version(module, version string) (string, error) { return "", fmt.Errorf("%q is no version: newest, a fingerprint (src2: and 64 hex digits) or at least its first 12 hex digits", version) } -// putAnswer is what `put` and `puts` answer of one put. +// putAnswer is what assigner_put and assigner_puts answer of one put. type putAnswer struct { Node string `json:"node"` Module string `json:"module"` @@ -118,8 +119,8 @@ type putAnswer struct { Follow string `json:"follow,omitempty"` } -// doPut is the verb put. -func (s *seat) doPut(args map[string]any, by string) (any, error) { +// doPut is the tool assigner_put. +func (s *putTools) doPut(args map[string]any, by string) (any, error) { node, module, why := arg(args, "node"), arg(args, "module"), arg(args, "why") key, err := delivery.PutKey(node, module) if err != nil { @@ -165,7 +166,7 @@ func (s *seat) doPut(args map[string]any, by string) (any, error) { } // await is the put at key once the assigner's process took it up past its asking, or as it stands after answerWithin. -func (s *seat) await(key string, p delivery.Put) delivery.Put { +func (s *putTools) await(key string, p delivery.Put) delivery.Put { deadline := time.After(answerWithin) for { s.mu.Lock() @@ -188,11 +189,11 @@ func (s *seat) await(key string, p delivery.Put) delivery.Put { // followPut says how a caller follows a put after the answer. func followPut(node, module string) string { return fmt.Sprintf("the put goes on: mesh_wait on mesh-delivery-assigner.put with filter {\"node\": %q, \"module\": %q}, then "+ - "mesh-delivery-assigner.puts for where it stands", node, module) + "mesh-delivery-assigner.assigner_puts for where it stands", node, module) } -// doRelease is the verb release. -func (s *seat) doRelease(args map[string]any, by string) (any, error) { +// doRelease is the tool assigner_release. +func (s *putTools) doRelease(args map[string]any, by string) (any, error) { node, module, why := arg(args, "node"), arg(args, "module"), arg(args, "why") key, err := delivery.PutKey(node, module) if err != nil { @@ -227,8 +228,8 @@ func (s *seat) doRelease(args map[string]any, by string) (any, error) { return putAnswer{Node: node, Module: module, Put: p}, nil } -// doPuts is the verb puts. -func (s *seat) doPuts(args map[string]any) (any, error) { +// doPuts is the tool assigner_puts. +func (s *putTools) doPuts(args map[string]any) (any, error) { keys, err := s.st.keys(putsState) if err != nil { return nil, fmt.Errorf("the puts cannot be listed: %w", err) @@ -263,21 +264,55 @@ func arg(args map[string]any, name string) string { return "" } -// tools are the seat's verbs as the runtime serves them. -func (s *seat) tools() []stdio.Tool { - verbs := map[string]catalogue.Verb{} - if seat, ok := catalogue.SeatNamed(catalogue.AssignerSeat); ok { - for _, v := range seat.Serves { - verbs[v.Name] = v +// The tools' names, as the manifest lists them. +const ( + toolPut = "assigner_put" + toolRelease = "assigner_release" + toolPuts = "assigner_puts" +) + +// schema is a tool's arguments, each a string: required ones first named, and switches true or false. +func schema(properties map[string]string, required []string, switches ...string) map[string]any { + props := map[string]any{} + for name, description := range properties { + p := map[string]any{"type": "string", "description": description} + if slices.Contains(switches, name) { + p["enum"] = []string{"true", "false"} } + props[name] = p } - tool := func(name string, run func(map[string]any) (any, error)) stdio.Tool { - return stdio.Tool{Name: catalogue.AssignerSeat + "." + name, Description: verbs[name].Description, Input: verbs[name].Input, Run: run} + out := map[string]any{"type": "object", "properties": props} + if len(required) > 0 { + out["required"] = required } + return out +} + +// tools are the assigner's tools as the runtime serves them (ADR 0546 clause 1). +func (s *putTools) tools() []stdio.Tool { return []stdio.Tool{ - tool("put", func(a map[string]any) (any, error) { return s.doPut(a, stdio.CallerOf(a)) }), - tool("release", func(a map[string]any) (any, error) { return s.doRelease(a, stdio.CallerOf(a)) }), - tool("puts", s.doPuts), + {Name: toolPut, Description: "Put one version of a module on one machine (novox/hq ADR 0546): the build at that fingerprint, " + + "the composition and grant the controller wrote for that machine, then the machine's want naming it, held there until " + + "released when hold is true. A module whose moves a person takes (the bus, a module of policy record) is asked of the " + + "operator first, and the bus has its streams snapshotted after the yes. Answers what was done or what it waits on; the " + + "put goes on after the answer, each step said as the event mesh-delivery-assigner.put, and assigner_puts says where it stands.", + Input: schema(map[string]string{ + "node": "the machine, where the module is assigned", + "module": "the module", + "version": "newest, a fingerprint (src2: and 64 hex digits), or at least its first 12 hex digits", + "hold": "\"true\": the want stays at this version until released; \"false\" (the default): the rules move it again once it runs", + "why": "why, kept with the put and said in any ask", + }, []string{"node", "module", "version", "why"}, "hold"), + Run: func(a map[string]any) (any, error) { return s.doPut(a, stdio.CallerOf(a)) }}, + {Name: toolRelease, Description: "End a put on one machine (novox/hq ADR 0546): its hold is taken away and the assigner's " + + "rules write the machine's want again from their next pass. Nothing changes on the machine until they move it.", + Input: schema(map[string]string{"node": "the machine", "module": "the module", "why": "why, kept with the put"}, + []string{"node", "module", "why"}), + Run: func(a map[string]any) (any, error) { return s.doRelease(a, stdio.CallerOf(a)) }}, + {Name: toolPuts, Description: "Every put and where it stands (novox/hq ADR 0546): its version, hold, why, each step with " + + "its time and words. Narrowed by machine or module. Read-only.", + Input: schema(map[string]string{"node": "one machine", "module": "one module"}, nil), + Run: s.doPuts}, } } @@ -299,9 +334,9 @@ func (runtimeStore) put(state, key string, value []byte) error { func (runtimeStore) keys(state string) ([]string, error) { return stdio.State(state).Keys() } -// serveSeat serves the seat's verbs to the runtime until it stops, hearing every put to answer `put` with its first steps. -func serveSeat() error { - s := newSeat(runtimeStore{}) +// serveTools serves the tools to the runtime until it stops, hearing every put to answer assigner_put with its first steps. +func serveTools() error { + s := newPutTools(runtimeStore{}) go func() { <-stdio.Served() for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 8d2add7..7a109b9 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.20261025195051-20a6dc4920f8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8 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 b0041d3..5164f2d 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024210118-748c564cdf7c h1:pY2YpHSS git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024210118-748c564cdf7c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025195051-20a6dc4920f8 h1:jA7V7hlfJAigVUvJDpKwfgd9ylQ9bqppg3rK9XgLWoA= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025195051-20a6dc4920f8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8 h1:vhYk/iv43u2WGSTrFyQGDvGBxLaRITgMAiiI9DjuA10= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8/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/internal/assign/put.go b/modules/mesh-delivery-assigner/internal/assign/put.go index eacc669..e6f13c8 100644 --- a/modules/mesh-delivery-assigner/internal/assign/put.go +++ b/modules/mesh-delivery-assigner/internal/assign/put.go @@ -8,11 +8,12 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner seat's verb. The verb writes the put +// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner's tool assigner_put. The tool writes the put // key; each pass takes it one step further from what the world holds (StepPut), and the rules pass over the node's want for -// the module from its asking until it ends (Module.Put). +// the module from its asking until it ends (Module.Puts). -// ComposeWithin is how long a put waits for the controller's composition after it asked for it before it is refused. +// ComposeWithin is how long a put waits for the composition and grant it needs (a take-in of a build just done writes them) +// before it is refused. const ComposeWithin = 10 * time.Minute // PutWorld is what one step of a put reads. @@ -27,14 +28,10 @@ type PutWorld struct { Build, BuildWords string // Newest is the module's newest fingerprint at the trunk, which the builder builds by itself. Newest string - // Composed is the controller's composition of the module for the node at the fingerprint; nil when there is none. + // Composed is the composition of the module for the node at the fingerprint, as its writer holds it; nil when there is + // none. Granted says the grant at the fingerprint names the node's accounts. Both are read, never asked for (clause 4). Composed *delivery.ComposedDigest - // Granted says the controller's grant at the fingerprint names the node's accounts. - Granted bool - // ComposeAsked is when this put asked the controller's compose verb, zero when it has not; ComposeFailed the words - // of its answer when it refused. - ComposeAsked time.Time - ComposeFailed string + Granted bool // Answer is the option the operator chose on the put's ask for the composition, "" when none. Answer string // Snapshot says where the bus's streams were snapshotted for this put, once they were; Snapshotting that it was @@ -50,8 +47,6 @@ type PutWorld struct { // PutNext is what a step decides: the put as it stands after it, and the acts the pass does for it. type PutNext struct { Put delivery.Put - // Compose asks the controller's compose verb for the composition at the fingerprint. - Compose bool // Ask writes the put's ask about the move (PutAsk). Ask bool // Snapshot has the bus's node back the bus up now, its dump the streams' snapshot (ADR 0235). @@ -88,20 +83,19 @@ func StepPut(w PutWorld) PutNext { return say(delivery.PutBuild, "waits for the builder's build of %s at %s, which is %s", w.Module, fp, w.Build) } if w.Composed == nil || !w.Granted { - switch { - case w.ComposeFailed != "": - return say(delivery.PutRefused, "the controller did not compose %s for %s at %s: %s", w.Module, w.Node, fp, w.ComposeFailed) - case w.ComposeAsked.IsZero(): - next := say(delivery.PutCompose, "asked the controller to issue the accounts of %s on %s, write its grant and compose it at %s", w.Module, w.Node, fp) - next.Compose = true - return next - case w.Now.Sub(w.ComposeAsked) > ComposeWithin: - return say(delivery.PutRefused, "the controller wrote no composition of %s for %s at %s with a grant naming %s within %s of the ask", w.Module, w.Node, fp, w.Node, ComposeWithin) - case w.Composed != nil: - return say(delivery.PutCompose, "waits for the controller's grant of %s at %s to name %s's accounts", w.Module, fp, w.Node) - default: - return say(delivery.PutCompose, "waits for the controller's composition of %s for %s at %s", w.Module, w.Node, fp) + missing := fmt.Sprintf("no composition of %s for %s at %s is written", w.Module, w.Node, fp) + if w.Composed != nil { + missing = fmt.Sprintf("the grant of %s at %s does not name %s's accounts", w.Module, fp, w.Node) } + since := w.Now + if p.Step == delivery.PutCompose && len(p.Steps) > 0 { + since = p.Steps[len(p.Steps)-1].At + } + if w.Now.Sub(since) > ComposeWithin { + return say(delivery.PutRefused, "%s after %s: only a composition written by the take-in of its build (or one of the five kept) is put; "+ + "for another the hand path is mesh-hand-compose (ADR 0539 §10)", missing, ComposeWithin) + } + return say(delivery.PutCompose, "waits up to %s: %s yet", ComposeWithin, missing) } d := *w.Composed if p.Step == delivery.PutWanted || p.Step == delivery.PutHolding { diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 942c194..c53f67d 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -492,8 +492,8 @@ func (m Module) userListRevision(node string, before delivery.Want, had bool, n return w, true } -// GrantNamesNode says whether a grant of module names its accounts on node (mesh-sdk's ControllerGrant.NamesNode, which the -// controller's compose verb answers by too). +// GrantNamesNode says whether a grant of module names its accounts on node (mesh-sdk's ControllerGrant.NamesNode, which a +// put reads by too). func GrantNamesNode(g delivery.ControllerGrant, node, module string) bool { return g.NamesNode(node, module) } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index e91e8e9..f1a3684 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -438,49 +438,44 @@ func stepsOfAPut(t *testing.T) { Composed: &d, Granted: true, Have: &running, Now: now} } cases := []struct { - name string - world func() PutWorld - step string - says string - compose, ask, snap bool - warranted bool + name string + world func() PutWorld + step string + says string + ask, snap bool + warranted bool }{ - {"not assigned", func() PutWorld { w := bus(); w.Assigned = false; return w }, delivery.PutRefused, "not assigned", false, false, false, false}, - {"no build yet of the newest", func() PutWorld { w := bus(); w.Build = ""; return w }, delivery.PutBuild, "builds by itself", false, false, false, false}, - {"no build of an older one", func() PutWorld { w := bus(); w.Build, w.Newest = "", fpOf('c'); return w }, delivery.PutRefused, "holds no build", false, false, false, false}, - {"a failed build", func() PutWorld { w := bus(); w.Build, w.BuildWords = delivery.BuildFailed, "go vet"; return w }, delivery.PutRefused, "go vet", false, false, false, false}, - {"no composition: asks the controller", func() PutWorld { w := bus(); w.Composed = nil; return w }, delivery.PutCompose, "asked the controller", true, false, false, false}, - {"composing", func() PutWorld { w := bus(); w.Composed, w.ComposeAsked = nil, now.Add(-time.Minute); return w }, delivery.PutCompose, "waits for the controller's composition", false, false, false, false}, - {"composed, grant not naming the node", func() PutWorld { w := bus(); w.Granted, w.ComposeAsked = false, now.Add(-time.Minute); return w }, delivery.PutCompose, "grant", false, false, false, false}, - {"the controller refused", func() PutWorld { + {"not assigned", func() PutWorld { w := bus(); w.Assigned = false; return w }, delivery.PutRefused, "not assigned", false, false, false}, + {"no build yet of the newest", func() PutWorld { w := bus(); w.Build = ""; return w }, delivery.PutBuild, "builds by itself", false, false, false}, + {"no build of an older one", func() PutWorld { w := bus(); w.Build, w.Newest = "", fpOf('c'); return w }, delivery.PutRefused, "holds no build", false, false, false}, + {"a failed build", func() PutWorld { w := bus(); w.Build, w.BuildWords = delivery.BuildFailed, "go vet"; return w }, delivery.PutRefused, "go vet", false, false, false}, + {"no composition yet: waits", func() PutWorld { w := bus(); w.Composed = nil; return w }, delivery.PutCompose, "no composition of nats for novox", false, false, false}, + {"composed, grant not naming the node", func() PutWorld { w := bus(); w.Granted = false; return w }, delivery.PutCompose, "does not name novox", false, false, false}, + {"none within the bound: refused", func() PutWorld { w := bus() - w.Composed, w.ComposeAsked, w.ComposeFailed = nil, now, "no lease" + w.Composed = nil + w.Put = w.Put.Said(now.Add(-ComposeWithin-time.Second), delivery.PutCompose, "waits") return w - }, delivery.PutRefused, "no lease", false, false, false, false}, - {"the controller silent too long", func() PutWorld { - w := bus() - w.Composed, w.ComposeAsked = nil, now.Add(-ComposeWithin-time.Second) - return w - }, delivery.PutRefused, "within", false, false, false, false}, - {"the bus asks the operator", bus, delivery.PutPerson, "waits for the operator's yes", false, true, false, false}, - {"the operator said no", func() PutWorld { w := bus(); w.Answer = OptionNo; return w }, delivery.PutRefused, "answered no", false, false, false, false}, - {"yes: the snapshot is asked", func() PutWorld { w := bus(); w.Answer = OptionYes; return w }, delivery.PutSnapshot, "asked novox's backup holder", false, false, true, false}, - {"the snapshot is taken", func() PutWorld { w := bus(); w.Answer, w.Snapshotting = OptionYes, true; return w }, delivery.PutSnapshot, "waits for novox's backup holder", false, false, false, false}, - {"the snapshot failed", func() PutWorld { w := bus(); w.Answer, w.SnapshotFailed = OptionYes, "no restic"; return w }, delivery.PutRefused, "no restic", false, false, false, false}, + }, delivery.PutRefused, "mesh-hand-compose", false, false, false}, + {"the bus asks the operator", bus, delivery.PutPerson, "waits for the operator's yes", true, false, false}, + {"the operator said no", func() PutWorld { w := bus(); w.Answer = OptionNo; return w }, delivery.PutRefused, "answered no", false, false, false}, + {"yes: the snapshot is asked", func() PutWorld { w := bus(); w.Answer = OptionYes; return w }, delivery.PutSnapshot, "asked novox's backup holder", false, true, false}, + {"the snapshot is taken", func() PutWorld { w := bus(); w.Answer, w.Snapshotting = OptionYes, true; return w }, delivery.PutSnapshot, "waits for novox's backup holder", false, false, false}, + {"the snapshot failed", func() PutWorld { w := bus(); w.Answer, w.SnapshotFailed = OptionYes, "no restic"; return w }, delivery.PutRefused, "no restic", false, false, false}, {"snapshotted: the want, warranted", func() PutWorld { w := bus() w.Answer, w.Snapshot = OptionYes, "novox's restore point" w.Put.Ask, _ = delivery.RecordAskKey("novox", "nats", d.Digest) return w - }, delivery.PutWanted, "held until released; the streams were snapshotted: novox's restore point", false, false, false, true}, + }, delivery.PutWanted, "held until released; the streams were snapshotted: novox's restore point", false, false, true}, {"a user-list revision of the bus needs nobody", func() PutWorld { w := bus() ul := d ul.UserListOnly = true w.Composed, w.Have = &ul, &delivery.Have{FP: fNew, Composed: dig(fOld), Phase: delivery.PhaseRunning, Health: delivery.HealthOK} return w - }, delivery.PutWanted, "held until released", false, false, false, false}, - {"a module not recorded needs nobody", func() PutWorld { w := bus(); w.Bus = false; return w }, delivery.PutWanted, "names", false, false, false, false}, + }, delivery.PutWanted, "held until released", false, false, false}, + {"a module not recorded needs nobody", func() PutWorld { w := bus(); w.Bus = false; return w }, delivery.PutWanted, "names", false, false, false}, {"a recorded module judged to recreate asks", func() PutWorld { w := bus() w.Bus, w.Recorded = false, true @@ -488,14 +483,14 @@ func stepsOfAPut(t *testing.T) { r.Judgement = delivery.JudgedRecreates w.Composed = &r return w - }, delivery.PutPerson, "waits for the operator's yes", false, true, false, false}, + }, delivery.PutPerson, "waits for the operator's yes", true, false, false}, {"wanted, the node runs it: holding", func() PutWorld { w := bus() w.Put = w.Put.Said(now, delivery.PutWanted, "wanted") w.Put.Composed = d.Digest w.Have = &delivery.Have{FP: fNew, Composed: d.Digest, Phase: delivery.PhaseRunning, Health: delivery.HealthOK} return w - }, delivery.PutHolding, "held there until released", false, false, false, false}, + }, delivery.PutHolding, "held there until released", false, false, false}, {"wanted without hold: done", func() PutWorld { w := bus() w.Put.Hold = false @@ -503,17 +498,17 @@ func stepsOfAPut(t *testing.T) { w.Put.Composed = d.Digest w.Have = &delivery.Have{FP: fNew, Composed: d.Digest, Phase: delivery.PhaseRunning, Health: delivery.HealthOK} return w - }, delivery.PutDone, "the rules write its want again", false, false, false, false}, - {"released: nothing more", func() PutWorld { w := bus(); w.Put = w.Put.Said(now, delivery.PutReleased, "released"); return w }, delivery.PutReleased, "released", false, false, false, false}, + }, delivery.PutDone, "the rules write its want again", false, false, false}, + {"released: nothing more", func() PutWorld { w := bus(); w.Put = w.Put.Said(now, delivery.PutReleased, "released"); return w }, delivery.PutReleased, "released", false, false, false}, } for _, c := range cases { next := StepPut(c.world()) if next.Put.Step != c.step || !strings.Contains(next.Put.Words, c.says) { t.Errorf("put, %s: at %s, %q; want %s saying %q", c.name, next.Put.Step, next.Put.Words, c.step, c.says) } - if next.Compose != c.compose || next.Ask != c.ask || next.Snapshot != c.snap || next.Warranted != c.warranted { - t.Errorf("put, %s: acts compose %v ask %v snapshot %v warranted %v; want %v %v %v %v", c.name, next.Compose, next.Ask, - next.Snapshot, next.Warranted, c.compose, c.ask, c.snap, c.warranted) + if next.Ask != c.ask || next.Snapshot != c.snap || next.Warranted != c.warranted { + t.Errorf("put, %s: acts ask %v snapshot %v warranted %v; want %v %v %v", c.name, next.Ask, next.Snapshot, next.Warranted, + c.ask, c.snap, c.warranted) } if next.Ask && next.Put.Ask == "" { t.Errorf("put, %s: an ask without its key", c.name) diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index fc528e4..a236dcf 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -56,18 +56,13 @@ func TestManifest(t *testing.T) { if !slices.Equal(m.Uses, []string{"node-build-go", "operator-channel"}) { t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses) } - // ADR 0546: it holds its seat (put, release, puts), served by its bundle through the tool runner (the artifact loads the - // module), says each put's step as the event put, and calls the controller's compose and warranted and the bus node's - // backup holder. - if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.AssignerSeat || len(m.Tools) != 0 { - t.Errorf("the assigner claims %v and serves tools %v, want its own seat alone", m.Claims, m.Tools) + // ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the + // module), claims no seat (a new seat needs a change of the controller), says each put's step as the event put, and + // calls the controller's warranted and the bus node's backup holder. + if len(m.Claims) != 0 || !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts"}) { + t.Errorf("the assigner claims %v and serves tools %v, want no claim and its three put tools", m.Claims, m.Tools) } - if seat, ok := catalogue.SeatNamed(catalogue.AssignerSeat); !ok { - t.Errorf("the sdk defines no seat %s", catalogue.AssignerSeat) - } else if err := catalogue.CanHold(m, seat); err != nil { - t.Errorf("the assigner cannot hold its seat: %v", err) - } - if want := []string{"seat:mesh-controller.compose", "seat:mesh-controller.warranted", "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { + if want := []string{"seat:mesh-controller.warranted", "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 bdf441a..3c31dc8 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -2,17 +2,6 @@ "module": "mesh-delivery-assigner", "version": "1", "slug": "assigner", - "claims": [ - { - "name": "mesh-delivery-assigner", - "scope": "mesh", - "serves": [ - "put", - "release", - "puts" - ] - } - ], "reads": [ "mesh-build-go.steps", "mesh-controller.assignments", @@ -32,7 +21,6 @@ "operator-channel" ], "invokes": [ - "seat:mesh-controller.compose", "seat:mesh-controller.warranted", "seat:node-backup.now", "seat:node-backup.backed-up" @@ -40,6 +28,11 @@ "emits": [ "put" ], + "tools": [ + "assigner_put", + "assigner_release", + "assigner_puts" + ], "state": [ { "name": "asks", "history": 5 }, { "name": "compositions", "history": 5 }, diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index b0bf04e..2b39b27 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025195051-20a6dc4920f8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8 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-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 2b4dabf..96b0283 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025195051-20a6dc4920f8 h1:jA7V7hlfJAigVUvJDpKwfgd9ylQ9bqppg3rK9XgLWoA= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025195051-20a6dc4920f8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8 h1:vhYk/iv43u2WGSTrFyQGDvGBxLaRITgMAiiI9DjuA10= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025200810-d4ae4b6790b8/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=