The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:13:20 +01:00
parent 369ebd092d
commit 0eafb2fe95
164 changed files with 1699 additions and 1498 deletions
@@ -303,7 +303,7 @@ func startBedWith(t *testing.T, closed bool) *bed {
viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...)
}
b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...)
viewstest.Serve(t, ctx, js, ctl, assignerModule, "placement", assignerModule+".assigner_read_placement", readers...)
viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...)
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1)
// The tag asks and their bounds (design 51 *Tagged versions*): the bounds declared key-ttl, so an ended bound leaves the
@@ -72,7 +72,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err != nil {
return err
}
// The indexer's modules, manifests, candidates and packages and the assigner's placement are their owners' states, read by the
// The indexer's modules, manifests, candidates and packages and the assigner's assignments are their owners' states, read by the
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, DLV-M1). An owner that
// is down never holds the start: each is read on a goroutine once its first view is answered, and until then the
// unanswered first view is the builder's finding.
@@ -105,7 +105,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err != nil {
return err
}
placement, err := reader.Follow(assignerModule, "placement", assignerModule+".assigner_read_placement")
assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments")
if err != nil {
return err
}
@@ -258,11 +258,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The toolchains first, so a build that starts from the first modules value finds the nodes.
watch := newToolchainWatch(ctx, js, tools, b.NodesChanged, logger)
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
// placement is settled first, so the toolchains settle through the gate already knowing it.
// assignments is settled first, so the toolchains settle through the gate already knowing it.
gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
watch.put = gate.Toolchain
// What each counted node's engine reads (ADR 0618 §2, ADR 0624 §1): asked through the node-engine seat's verb as the
// builder's own account, for every node the placement names, and kept in the state engine-reads once it is bound.
// builder's own account, for every node the assignments names, and kept in the state engine-reads once it is bound.
reads := newEngineReads(calls.CallOn, gate, logger.Printf)
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
// reads it and drops a retired node by hand.
@@ -288,20 +288,20 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
})
gate.OnPlaced = reads.placed
b.Forms = reads.built
// Until the placement's first view is answered the gate passes every report on, as before the placement is settled.
placementErrs := make(chan error, 1)
// Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
assignmentsErrs := make(chan error, 1)
go func() {
select {
case <-ctx.Done():
return
case <-placement.Follower().Ready():
case <-assignments.Follower().Ready():
}
pe, err := buildrun.FollowPlacement(ctx, reader, gate, b.NodesChanged, logger.Printf)
pe, err := buildrun.FollowAssignments(ctx, reader, gate, b.NodesChanged, logger.Printf)
if err != nil {
placementErrs <- err
assignmentsErrs <- err
return
}
placementErrs <- <-pe
assignmentsErrs <- <-pe
}()
if err := watch.start(); err != nil {
return err
@@ -456,7 +456,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return nil
case err := <-errs:
return err
case err := <-placementErrs:
case err := <-assignmentsErrs:
return err
case err := <-watch.errs:
return err
@@ -16,7 +16,7 @@ const (
// indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows.
indexerModule = "mesh-delivery-indexer"
// assignerModule is the holder of the state placement.
// assignerModule is the holder of the state assignments.
assignerModule = buildrun.AssignerModule
// sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names.
@@ -33,7 +33,7 @@ func TestTheNamesAreTheManifests(t *testing.T) {
}
}
for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates",
indexerModule + ".views.packages", assignerModule + ".views.placement"} {
indexerModule + ".views.packages", assignerModule + ".views.assignments"} {
if !slices.Contains(m.Consumes, view) {
t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes)
}
@@ -41,7 +41,7 @@ func TestTheNamesAreTheManifests(t *testing.T) {
for want, got := range map[string]string{
"mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
"mesh-build-node_toolchains": buildrun.ToolchainsBucket("mesh-build-node"), "mesh-delivery-indexer_packages": packagesBucket,
"mesh-delivery-assigner_placement": buildrun.PlacementBucket,
"mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket,
} {
if want != got {
t.Errorf("a bucket is %q, want %q", got, want)
@@ -21,7 +21,7 @@ import (
)
// oldForm keeps the old reference form's window (design 51 *Every build keeps the old reference form*, DLV-B9) in the
// state old-form: stepped on every change of what the counted nodes state, of the placement, on a hand drop and at each
// state old-form: stepped on every change of what the counted nodes state, of the assignments, on a hand drop and at each
// build key written, never on a timer. A node holding it seven days is the finding old-form-held; its close is said once
// as version.old-form-closed and in the builder's log.
type oldForm struct {
@@ -84,7 +84,7 @@ func (o *oldForm) dropped() map[string]bool {
}
// step moves the window to what counted states.
// named is every node the read named (the placement's nodes before any is dropped), counted the statements of those
// named is every node the read named (the assignments's nodes before any is dropped), counted the statements of those
// still counted: a read naming no node judges nothing (window.StepWindow), so it never closes the window.
func (o *oldForm) step(ctx context.Context, named []string, counted map[string][]string) {
o.mu.Lock()
@@ -106,7 +106,7 @@ func (o *oldForm) step(ctx context.Context, named []string, counted map[string][
n, next.Since[n].Format(time.RFC3339)),
headline: "A node holds the old reference form",
explanation: "The builder writes an older reference form into every build key while a counted node's engine reads none of the newest (ADR 0624 §1); this node has held it seven days.",
resolved: "The node's engine states the newest form, the node leaves the placement, or it is dropped as retired.",
resolved: "The node's engine states the newest form, the node leaves the assignments, or it is dropped as retired.",
needs: "builder_old_form shows the window; a retired node is dropped there by hand; builder_engine_reads asks the node again."})
}
o.overdue = overdue
@@ -10,14 +10,14 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
)
// EngineReadsState is the builder's state of what each counted node's engine states it reads (novox/hq ADR 0618 §2, ADR
// 0624 §1): one key `<node>`, written from the node-engine seat's verb manifest-reads at the builder's start, on the
// seat's event manifest-reads-changed for that node, and when a person asks again (builder_engine_reads); a node that
// is away keeps its last statement, and its key is removed when the placement no longer names the node. A counted node
// is away keeps its last statement, and its key is removed when the assignments no longer names the node. A counted node
// with no key reads as stating the baseline: its first forms. No node's record is read in its place.
const EngineReadsState = "engine-reads"
@@ -43,7 +43,7 @@ func newEngineReads(call enginereads.Caller, gate *buildrun.ToolchainGate, logf
}
// stepWindow moves the old form's window to what the counted nodes state now (design 51 *Every build keeps the old
// reference form*): on a statement's change, the placement's and a hand drop, and at each build key written.
// reference form*): on a statement's change, the assignments's and a hand drop, and at each build key written.
func (r *engineReads) stepWindow() {
if r.window == nil {
return
@@ -56,7 +56,7 @@ func (r *engineReads) stepWindow() {
r.window.step(ctx, r.named(), counted)
}
// named are the nodes the placement names, none dropped: what the window's step judges a read by.
// named are the nodes the assignments names, none dropped: what the window's step judges a read by.
func (r *engineReads) named() []string {
r.mu.Lock()
defer r.mu.Unlock()
@@ -86,7 +86,7 @@ func (r *engineReads) bind(ctx context.Context, kv jetstream.KeyValue, errs chan
}
}
// placed takes the placement in: a node it names newly is asked, one it no longer names is forgotten, key and all. The
// placed takes the assignments in: a node it names newly is asked, one it no longer names is forgotten, key and all. The
// first call asks every counted node (the builder's start).
func (r *engineReads) placed() {
nodes := r.gate.Nodes()
@@ -96,7 +96,7 @@ func (r *engineReads) placed() {
r.mu.Unlock()
for _, node := range nodes {
if first || !slices.Contains(before, node) {
r.keeper.AskNode(node, "counted: the placement names it")
r.keeper.AskNode(node, "counted: the assignments names it")
}
}
for _, node := range before {
@@ -20,15 +20,15 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
@@ -36,7 +36,7 @@ import (
// 1945 (a part exits at start for a bucket it works without): a bus that refuses every toolchains bucket leaves the
// watch unstarted and the part running, said; a refused bucket it cannot work without is an error naming the bucket and
// its grant. The gate over the seat's placement, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
// issue 774).
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
@@ -69,28 +69,28 @@ func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
t.Run("ADR 0624: the counted nodes' engine reads", theCountedNodesEngineReads)
}
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the placement names; a node away
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the assignments names; a node away
// and never asked has no statement and reads as the baseline's first forms, with no node record read in its place; an
// away node keeps its last statement; a node the placement no longer names leaves the count, its key removed; and
// away node keeps its last statement; a node the assignments no longer names leaves the count, its key removed; and
// builder_engine_reads drops one node's key.
func theCountedNodesEngineReads(t *testing.T) {
gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf)
place := func(nodes ...string) {
for _, node := range []string{"n1", "n2"} {
pl := meshmoduleassigner.Placement{Node: node}
pl := meshmoduleassigner.Assignments{Node: node}
if slices.Contains(nodes, node) {
pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}}
}
values, err := meshmoduleassignerclient.PlacementValues(pl)
values, err := meshmoduleassignerclient.AssignmentsValues(pl)
if err != nil {
t.Fatal(err)
}
for key, raw := range values {
gate.Placement(key, raw, false, time.Now())
gate.Assignments(key, raw, false, time.Now())
}
if !slices.Contains(nodes, node) {
key, _ := meshmoduleassignerclient.ModuleKey(node, "web")
gate.Placement(key, nil, true, time.Now())
gate.Assignments(key, nil, true, time.Now())
}
}
}
@@ -125,7 +125,7 @@ func theCountedNodesEngineReads(t *testing.T) {
if _, counted = r.forms(); !slices.Equal(counted["n2"], []string{"form:new"}) || r.keeper.Statements()["n2"].Answered {
t.Errorf("away again, n2 reads %v (%+v)", counted["n2"], r.keeper.Statements()["n2"])
}
// n2 retired: it leaves the placement, and its key goes.
// n2 retired: it leaves the assignments, and its key goes.
place("n1")
r.placed()
if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil {