From b1fcabcebe549e550546bd803fadef16889fe22c Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 11:57:53 +0100 Subject: [PATCH] AS3: a placement's branch and build, set by a proposal, bound by the ask, and its build a held put MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A proposal sets a module's watched branch and build (a re-place keeps them), the changes digest an ask binds to carries both, and a placement's build is a held put of that build, released when the placement drops it while a person's put stays. Taken over from 6bc6fe53 and b8e50fff. Builds: design 47 G §14, §78, §114, §360, §361 Tests: TestAssignments (place change branch, build, digest rows), aPlacementsBuildIsPut. Test 77 (§79) covered by the put row. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-delivery-assigner/pass_test.go | 58 +++++++++++ .../mesh-delivery-assigner/placementbuild.go | 95 +++++++++++++++++++ .../cmd/mesh-delivery-assigner/puts.go | 1 + .../internal/assignment/assignments_test.go | 18 ++++ .../internal/assignment/digest.go | 8 +- .../internal/assignment/proposal.go | 10 +- 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/placementbuild.go 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 0bc60e9a..5e8e1ce4 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 @@ -272,6 +272,7 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) { t.Error("an answer to an ask taken away was taken") } t.Run("a put of the bus acts on the operator's warrant alone", putOfTheBus) + t.Run("a placement's build is put held, and released when the placement names it no more", aPlacementsBuildIsPut) // ADR 0614 §4: the rules read what each node's engine states it reads from its record in `.node`; a node with no // record, or one that states nothing, is read as stating nothing (the rules then read the baseline). t.Run("the rules read what each node's engine reads", func(t *testing.T) { @@ -467,6 +468,63 @@ func (b *fakeBus) Emit(eventType string, body any) error { return nil } +// Design 47 line 912 and line 914 ("a placement's `build` delivers the earlier build"; ADR 0666 §24, B-K1): a placed +// module's build becomes a held put of that build, which then goes a put's steps; a placement naming no build any more +// releases the put it made, never a person's. +func aPlacementsBuildIsPut(t *testing.T) { + ctx := context.Background() + putsKV := &memKV{values: map[string][]byte{}} + p := newPasser(kvSet{buckets.Puts: putsKV}, time.Date(2026, 11, 10, 18, 0, 0, 0, time.UTC)) + earlier := meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64) + place := func(build string) { + values, err := mmac.AssignmentsValues(mma.Assignments{Node: "g14", Modules: []mma.PlacedModule{{Module: "web", Build: build}, {Module: "db"}}}) + if err != nil { + t.Fatal(err) + } + for key, raw := range values { + p.w.putEntry(plc.AssignmentsBucket, key, raw, uint64(len(raw)), time.Now()) + } + } + read := func() (puts.Put, bool) { + raw, ok := putsKV.values["g14.web"] + if !ok { + return puts.Put{}, false + } + got, err := puts.Decode(raw) + if err != nil { + t.Fatal(err) + } + return got, true + } + place(earlier) + p.placementPuts(ctx) + got, ok := read() + if !ok || got.FP != earlier || !got.Hold || got.By != placementBy || got.Step != puts.Asked { + t.Fatalf("the placement's build made the put %+v (there %v)", got, ok) + } + if _, there := putsKV.values["g14.db"]; there { + t.Error("a placed module naming no build was put") + } + p.placementPuts(ctx) // again: the same build makes nothing new + if again, _ := read(); !again.At.Equal(got.At) || again.Step != puts.Asked { + t.Errorf("a second pass made the put again: %+v", again) + } + place("") + p.placementPuts(ctx) + if got, _ := read(); got.Step != puts.Released || !strings.Contains(got.Words, placementBy) { + t.Errorf("the placement naming no build left the put %+v", got) + } + // A person's put stands while the placement names no build. + person := puts.Put{FP: earlier, Hold: true, Why: "a try", By: "lead@shanks", At: time.Now()}.Said(time.Now(), puts.Holding, "held") + raw, _ := puts.Encode(person) + putsKV.values["g14.web"] = raw + p.w.put(buckets.Puts, "g14.web", raw) + p.placementPuts(ctx) + if got, _ := read(); got.Step != puts.Holding || got.By != "lead@shanks" { + t.Errorf("a person's put was released by the placement: %+v", got) + } +} + // 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 diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/placementbuild.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/placementbuild.go new file mode 100644 index 00000000..3fb5d39c --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/placementbuild.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "fmt" + "sort" + + "mesh-delivery-assigner/internal/buckets" + "mesh-delivery-assigner/internal/puts" +) + +// placementBy is who a put the placement's build makes names as its asker: the placement, whose proposal the operator +// approved, is the warrant (design 47 *Going back is one act*; ADR 0666 §24). +const placementBy = "the placement" + +// placementPuts makes each placed module's `build` a held put of that build, and releases a put the placement made once its +// placement names that build no more (design 47 line 912: "a return to an earlier build is one value in the placement, the +// module's `build` in its placement key, set by a placement proposal"; ADR 0666 §24; B-K1). The put then goes its own steps +// (the builder's build, the composition, an ask where the module's policy needs one, the want, holding) and is read with +// `assigner_puts` and released with `assigner_release` like any other; a placement's build the builder never built is refused +// by its build step, said in the put's words. A put asked by a person stands until the placement names another build. +func (p *passer) placementPuts(ctx context.Context) { + kv := p.kvs[buckets.Puts] + if kv == nil { + return + } + now := p.clock().UTC() + named := map[string]string{} // put key -> the build its placement names + for node, a := range p.w.assignments() { + for _, m := range a.Modules { + if m.Build == "" { + continue + } + if key, err := puts.Key(node, m.Module); err == nil { + named[key] = m.Build + } + } + } + held := p.w.read(buckets.Puts) + keys := make([]string, 0, len(named)+len(held)) + for k := range named { + keys = append(keys, k) + } + for k := range held { + if _, there := named[k]; !there { + keys = append(keys, k) + } + } + sort.Strings(keys) + for _, key := range keys { + build, names := named[key] + var before *puts.Put + if raw, ok := held[key]; ok { + if got, err := puts.Decode(raw); err == nil { + before = &got + } + } + var next puts.Put + switch { + case names && before != nil && before.FP == build: + continue // made already, or a person's put of the same build: it goes its own steps, ended or not + case names: + words := fmt.Sprintf("asked by %s: it names build %s of %s", placementBy, shortFP(build), key) + if before != nil && !puts.Ended(before.Step) { + words += fmt.Sprintf("; it replaces the put of %s, which stood at %s", shortFP(before.FP), before.Step) + } + next = puts.Put{FP: build, Hold: true, Why: "the placement names this build (ADR 0666 §24)", By: placementBy, At: now}.Said(now, puts.Asked, words) + case before != nil && before.By == placementBy && !puts.Ended(before.Step): + next = before.Said(now, puts.Released, fmt.Sprintf("released by %s: it names no build of its own any more; the assigner's rules write the want again", placementBy)) + default: + continue + } + data, err := puts.Encode(next) + if err != nil { + p.logf("the placement's put %s is not written: %v", key, err) + continue + } + if before == nil { + _, err = kv.Create(ctx, key, data) + } else if e, gerr := kv.Get(ctx, key); gerr != nil { + err = gerr + } else { + _, err = kv.Update(ctx, key, data, e.Revision()) + } + if err != nil { + p.logf("the placement's put %s could not be written, and is written at the next pass: %v", key, err) + continue + } + p.w.put(buckets.Puts, key, data) + p.logf("put %s: %s: %s", key, next.Step, next.Words) + if p.acts != nil { + p.acts.say(key, next) + } + } +} 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 929ecbdb..77ccfaf2 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -105,6 +105,7 @@ func (p *passer) puts(ctx context.Context) { if p.acts == nil { return } + p.placementPuts(ctx) all := p.putsByModule() if len(all) == 0 { return diff --git a/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go b/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go index 13789020..33a30427 100644 --- a/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go +++ b/modules/mesh-delivery-assigner/internal/assignment/assignments_test.go @@ -129,6 +129,24 @@ func TestAssignments(t *testing.T) { if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) { t.Error("laying changed the current assignments") } + // Design 47 line 1051 (the Choices table: a module's watched branch and its build in the placement key, set by a + // proposal): a place change sets them, and a re-place naming none keeps the module's. + onBeta, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Branch: "beta", Build: "b1"}}) + again, g := LayChanges(onBeta, []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Ports: map[string]int64{"80": 8081}}}) + if m := onBeta["g14"].Modules[0]; len(f) != 0 || m.Branch != "beta" || m.Build != "b1" { + t.Errorf("a place change's branch and build: %+v, %v", m, f) + } + if m := again["g14"].Modules[0]; len(g) != 0 || m.Branch != "beta" || m.Build != "b1" || m.Ports["80"] != 8081 { + t.Errorf("a re-place dropped the branch and build: %+v, %v", m, g) + } + if ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Branch: "beta"}}}) == + ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh"}}}) { + t.Error("an ask bound to a place change's digest does not bind its branch") + } + if ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Build: "b1"}}}) == + ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh"}}}) { + t.Error("an ask bound to a place change's digest does not bind its build (design 47 G 360)") + } if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" { t.Errorf("a node with no key: %+v, %v", fresh, f) } diff --git a/modules/mesh-delivery-assigner/internal/assignment/digest.go b/modules/mesh-delivery-assigner/internal/assignment/digest.go index b22e05c6..a7166a1a 100644 --- a/modules/mesh-delivery-assigner/internal/assignment/digest.go +++ b/modules/mesh-delivery-assigner/internal/assignment/digest.go @@ -29,6 +29,8 @@ type placedForm struct { HasLayer bool `json:"has-layer,omitempty"` MeshValues map[string]any `json:"mesh-wide-values,omitempty"` Values map[string]any `json:"values,omitempty"` + Branch string `json:"branch,omitempty"` + Build string `json:"build,omitempty"` } type holdingForm struct { @@ -110,6 +112,8 @@ type changeForm struct { Whole *assignmentsForm `json:"whole,omitempty"` Decided *decidedForm `json:"decided,omitempty"` Keys *joiningForm `json:"keys,omitempty"` + Branch string `json:"branch,omitempty"` + Build string `json:"build,omitempty"` } func providerOf(p seat.Provider) providerForm { return providerForm{Node: p.Node, Module: p.Module} } @@ -150,7 +154,7 @@ func assignmentsOf(p seat.Assignments) assignmentsForm { } for _, m := range p.Modules { out.Modules = append(out.Modules, placedForm{Module: m.Module, Ports: m.Ports, Bindings: providersOf(m.Bindings), - MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values}) + MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values, Branch: m.Branch, Build: m.Build}) } for _, h := range p.Holds { out.Holds = append(out.Holds, holdingForm{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind}) @@ -161,7 +165,7 @@ func assignmentsOf(p seat.Assignments) assignmentsForm { func changeOf(c seat.ProposalChange) changeForm { out := changeForm{Op: c.Op, Node: c.Node, Module: c.Module, Ports: c.Ports, Bindings: providersOf(c.Bindings), Provision: c.Provision, Claim: c.Claim, Scope: c.Scope, Kind: c.Kind, Site: c.Site, Field: c.Field, MeshWide: c.MeshWide, Layer: c.Layer, - MeshValues: c.MeshWideValues, Values: c.Values} + MeshValues: c.MeshWideValues, Values: c.Values, Branch: c.Branch, Build: c.Build} if c.Provider != nil { p := providerOf(*c.Provider) out.Provider = &p diff --git a/modules/mesh-delivery-assigner/internal/assignment/proposal.go b/modules/mesh-delivery-assigner/internal/assignment/proposal.go index b66cf164..1680e598 100644 --- a/modules/mesh-delivery-assigner/internal/assignment/proposal.go +++ b/modules/mesh-delivery-assigner/internal/assignment/proposal.go @@ -148,11 +148,19 @@ func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChan } continue case client.OpPlace: - m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings} + // A module's watched branch and its build are in the placement key, set by a proposal (design 47 line 1051, + // the Choices table's row): the change's when it names them, else kept from the module placed there. + m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings, Branch: c.Branch, Build: c.Build} if at >= 0 { o := p.Modules[at] m.MeshWide, m.HasMeshWide, m.Layer, m.HasLayer = o.MeshWide, o.HasMeshWide, o.Layer, o.HasLayer m.MeshWideValues, m.Values = o.MeshWideValues, o.Values + if m.Branch == "" { + m.Branch = o.Branch + } + if m.Build == "" { + m.Build = o.Build + } p.Modules[at] = m } else { p.Modules = append(p.Modules, m)