AS3: a placement's branch and build, set by a proposal, bound by the ask, and its build a held put
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
This commit is contained in:
@@ -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>.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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user