Put a version of a module on a node with one tool call: the assigner's put, release and puts (ADR 0546) #49

Merged
mesh-tools merged 5 commits from feat/put-version into main 2026-10-25 20:52:06 +00:00
13 changed files with 186 additions and 207 deletions
Showing only changes of commit 45fae9da8a - Show all commits
@@ -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)
}
@@ -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)
@@ -16,12 +16,12 @@ import (
"mesh-delivery-assigner/internal/assign"
)
// The puts (novox/hq ADR 0546): the seat's verb `put` writes the key `<node>.<module>` 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 `<node>.<module>` 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.
@@ -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) {
+1 -1
View File
@@ -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
)
+2
View File
@@ -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=
@@ -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 {
@@ -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)
}
@@ -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)
@@ -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"}) {
+5 -12
View File
@@ -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 },
+1 -1
View File
@@ -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
)
+2
View File
@@ -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=