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
+13 -13
View File
@@ -5,7 +5,7 @@
// view may answer them: the hold of a fingerprint whose health check failed (`hold.<module>.<fingerprint hex>`: release it or // view may answer them: the hold of a fingerprint whose health check failed (`hold.<module>.<fingerprint hex>`: release it or
// keep it held), the restore of a data module set back (`restore.<node>.<module>.<fingerprint hex>`: restore and lose newer // keep it held), the restore of a data module set back (`restore.<node>.<module>.<fingerprint hex>`: restore and lose newer
// writes, or leave it stopped, ADR 0429 §6), a backup that failed before an apply, a move a person must allow for a module of // writes, or leave it stopped, ADR 0429 §6), a backup that failed before an apply, a move a person must allow for a module of
// policy record, and the authorising ask of a proposal of placement. // policy record, and the authorising ask of a proposal of assignments.
package asks package asks
import ( import (
@@ -14,8 +14,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -34,15 +34,15 @@ const (
// AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: a // AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: a
// composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14). // composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14).
AskKindRecord = "record" AskKindRecord = "record"
// AskKindPlacement is the kind of the authorising ask about a proposal of placement (novox/hq ADR 0567 §5): // AskKindAssignments is the kind of the authorising ask about a proposal of assignments (novox/hq ADR 0567 §5):
// `placement.<proposal id>`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a // `assignments.<proposal id>`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a
// channel proving who answered takes and the view never does (ViewAnswerFits). // channel proving who answered takes and the view never does (ViewAnswerFits).
AskKindPlacement = "placement" AskKindAssignments = "assignments"
) )
// AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module
// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the // and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the
// composition it would allow, a placement's Proposal. // composition it would allow, the assignments' Proposal.
type AskKey struct { type AskKey struct {
Proposal string Proposal string
Kind string Kind string
@@ -60,11 +60,11 @@ func AskNeedsProof(kind string) (bool, error) {
switch kind { switch kind {
case meshdeliveryclient.AskKindMerge: case meshdeliveryclient.AskKindMerge:
return true, nil return true, nil
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement: case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments:
return false, nil return false, nil
} }
return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold,
AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement) AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments)
} }
// HoldAskKey is the key of the ask about a module's held fingerprint: `hold.<module>.<hex>`. // HoldAskKey is the key of the ask about a module's held fingerprint: `hold.<module>.<hex>`.
@@ -103,12 +103,12 @@ func BackupAskKey(node, module, fingerprint string) (string, error) {
return AskKindBackup + "." + key, nil return AskKindBackup + "." + key, nil
} }
// PlacementAskKey is the key of the authorising ask about a proposal of placement: `placement.<proposal id>` (ADR 0567 §5). // AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.<proposal id>` (ADR 0567 §5).
func PlacementAskKey(proposal string) (string, error) { func AssignmentsAskKey(proposal string) (string, error) {
if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil { if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil {
return "", err return "", err
} }
return AskKindPlacement + "." + proposal, nil return AskKindAssignments + "." + proposal, nil
} }
// RecordAskKey is the key of the ask about a move of a module on a node to the composition whose digest it names: // RecordAskKey is the key of the ask about a move of a module on a node to the composition whose digest it names:
@@ -124,7 +124,7 @@ func RecordAskKey(node, module, composed string) (string, error) {
return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil
} }
// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or placement ask); it refuses an unknown // ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or assignments ask); it refuses an unknown
// kind and a key whose parts do not fit its kind, naming the key. // kind and a key whose parts do not fit its kind, naming the key.
func ParseAnyAskKey(key string) (AskKey, error) { func ParseAnyAskKey(key string) (AskKey, error) {
kind, rest, _ := strings.Cut(key, ".") kind, rest, _ := strings.Cut(key, ".")
@@ -167,7 +167,7 @@ func ParseAnyAskKey(key string) (AskKey, error) {
return refused(err) return refused(err)
} }
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
case AskKindPlacement: case AskKindAssignments:
if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil { if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil {
return refused(err) return refused(err)
} }
@@ -17,17 +17,17 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) {
hold, _ := HoldAskKey("gitea", fp) hold, _ := HoldAskKey("gitea", fp)
restore, _ := RestoreAskKey("n1", "gitea", fp) restore, _ := RestoreAskKey("n1", "gitea", fp)
backup, _ := BackupAskKey("n1", "novox.be", fp) backup, _ := BackupAskKey("n1", "novox.be", fp)
placement, _ := PlacementAskKey("p-20261028t183000-1a2b3c") assignments, _ := AssignmentsAskKey("p-20261028t183000-1a2b3c")
record, _ := RecordAskKey("n1", "gitea", digest) record, _ := RecordAskKey("n1", "gitea", digest)
for key, want := range map[string]struct { for key, want := range map[string]struct {
got string got string
back AskKey back AskKey
}{ }{
"hold": {hold, AskKey{Kind: "hold", Module: "gitea", FP: fp}}, "hold": {hold, AskKey{Kind: "hold", Module: "gitea", FP: fp}},
"restore": {restore, AskKey{Kind: "restore", Node: "n1", Module: "gitea", FP: fp}}, "restore": {restore, AskKey{Kind: "restore", Node: "n1", Module: "gitea", FP: fp}},
"backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}}, "backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}},
"placement": {placement, AskKey{Kind: "placement", Proposal: "p-20261028t183000-1a2b3c"}}, "assignments": {assignments, AskKey{Kind: "assignments", Proposal: "p-20261028t183000-1a2b3c"}},
"record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}}, "record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}},
} { } {
back, err := ParseAnyAskKey(want.got) back, err := ParseAnyAskKey(want.got)
if err != nil || back != want.back { if err != nil || back != want.back {
@@ -35,7 +35,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) {
} }
} }
for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex, for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex,
placement: "placement.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} { assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} {
if got != want { if got != want {
t.Errorf("key %q, want %q", got, want) t.Errorf("key %q, want %q", got, want)
} }
@@ -45,7 +45,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) {
t.Errorf("a merge ask key: %+v, %v", merge, err) t.Errorf("a merge ask key: %+v, %v", merge, err)
} }
for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60], for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60],
"placement.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} { "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} {
if _, err := ParseAnyAskKey(bad); err == nil { if _, err := ParseAnyAskKey(bad); err == nil {
t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad) t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad)
} }
+2 -2
View File
@@ -4,7 +4,7 @@ go 1.26.0
require ( require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
@@ -14,7 +14,7 @@ require (
require ( require (
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect
@@ -9,8 +9,8 @@ import (
"fmt" "fmt"
"io" "io"
"maps" "maps"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"mesh-delivery-assigner/internal/puts" "mesh-delivery-assigner/internal/puts"
"os" "os"
"path/filepath" "path/filepath"
@@ -21,10 +21,10 @@ import (
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mdcc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" mdcc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -37,10 +37,10 @@ import (
"mesh-delivery-assigner/internal/judge" "mesh-delivery-assigner/internal/judge"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/announce"
@@ -56,7 +56,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// bed is an embedded bus with JetStream, every bucket the assigner reads and keeps, and the stream the mesh's events // bed is an embedded bus with JetStream, every bucket the assigner reads and keeps, and the stream the mesh's events
@@ -67,7 +67,7 @@ type bed struct {
ctx context.Context ctx context.Context
dir string dir string
buckets map[string]jetstream.KeyValue buckets map[string]jetstream.KeyValue
// placed is, by node, the modules the bed's placement names on it (place). // placed is, by node, the modules the bed's assignments names on it (place).
placed map[string][]string placed map[string][]string
} }
@@ -229,7 +229,7 @@ func saysWhatItKeeps(t *testing.T, b *bed, done <-chan error) {
func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) { func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) {
for _, missing := range readBuckets { for _, missing := range readBuckets {
if startsWithout(missing) { if startsWithout(missing) {
continue // it starts without it (the placement and the grants' source: main_test.go; the puts: below) continue // it starts without it (the assignments and the grants' source: main_test.go; the puts: below)
} }
b := startBed(t, missing) b := startBed(t, missing)
select { select {
@@ -278,26 +278,36 @@ func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) {
} }
} }
// Without the seat's placement state it starts all the same, and once the bucket is made (the controller makes // Without the seat's assignments state it starts all the same, and once the bucket is made (the controller makes
// it from the manifest) it is read with no restart: a node it names gets its own want, the node composing its own. // it from the manifest) it is read with no restart: a node it names gets its own want, the node composing its own.
pb := startBed(t, plc.PlacementBucket) pb := startBed(t, plc.AssignmentsBucket)
pdone := pb.start(t) pdone := pb.start(t)
select { select {
case err := <-pdone: case err := <-pdone:
t.Fatalf("without %s the assigner stopped: %v", plc.PlacementBucket, err) t.Fatalf("without %s the assigner stopped: %v", plc.AssignmentsBucket, err)
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
} }
placement, err := pb.js.CreateKeyValue(pb.ctx, jetstream.KeyValueConfig{Bucket: plc.PlacementBucket, History: 5}) // ADR 0641: the state the seat's assignments were kept in before holds n1's keys, and the form of one key per node; the
// assigner copies the keys into the state made after its start, drops the second form, and leaves the old state empty.
values, _ := mmac.AssignmentsValues(mma.Assignments{Node: "n1", Modules: []mma.PlacedModule{{Module: "web"}}})
for _, k := range slices.Sorted(maps.Keys(values)) { // a key per module, then the node key
pb.putJSON(t, retiredBucket, k, values[k])
}
pb.putJSON(t, retiredBucket, "placement.n1", values["n1.node"])
assignments, err := pb.js.CreateKeyValue(pb.ctx, jetstream.KeyValueConfig{Bucket: plc.AssignmentsBucket, History: 5})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
pl, _ := mmac.EncodePlacement(mma.Placement{Node: "n1", Modules: []mma.PlacedModule{{Module: "web"}}})
plkey, _ := mmac.LegacyKey("n1") // the form before, read beside the new (research 072 row 37)
if _, err := placement.Put(pb.ctx, plkey, pl); err != nil {
t.Fatal(err)
}
if nw, err := mwc.DecodeNodeWant(pb.get(t, buckets.Wants, "want.n1.node")); err != nil || !nw.Self { if nw, err := mwc.DecodeNodeWant(pb.get(t, buckets.Wants, "want.n1.node")); err != nil || !nw.Self {
t.Fatalf("the placement state made after the start was not read: n1's own want = %+v, %v", nw, err) t.Fatalf("the assignments state made after the start was not read: n1's own want = %+v, %v", nw, err)
}
for _, k := range slices.Sorted(maps.Keys(values)) {
if e, err := assignments.Get(pb.ctx, k); err != nil || !slices.Equal(e.Value(), values[k]) {
t.Errorf("%s was not copied into %s: %v", k, plc.AssignmentsBucket, err)
}
}
if left, err := pb.buckets[retiredBucket].Keys(pb.ctx); err == nil || !errors.Is(err, jetstream.ErrNoKeysFound) {
t.Errorf("the retired state still holds %v (%v)", left, err)
} }
} }
@@ -309,8 +319,8 @@ func (b *bed) putJSON(t *testing.T, bucket, key string, value []byte) {
} }
} }
// place adds module to the seat's placement of node, written as the seat writes it: one node key and a key per module // place adds module to the seat's assignments of node, written as the seat writes it: one node key and a key per module
// (research 072 row 37). The placement alone names where modules run (ADR 0567 §8). // (research 072 row 37). The assignments alone names where modules run (ADR 0567 §8).
func (b *bed) place(t *testing.T, node, module string) { func (b *bed) place(t *testing.T, node, module string) {
t.Helper() t.Helper()
if b.placed == nil { if b.placed == nil {
@@ -319,16 +329,16 @@ func (b *bed) place(t *testing.T, node, module string) {
if !slices.Contains(b.placed[node], module) { if !slices.Contains(b.placed[node], module) {
b.placed[node] = append(b.placed[node], module) b.placed[node] = append(b.placed[node], module)
} }
pl := mma.Placement{Node: node} pl := mma.Assignments{Node: node}
for _, m := range b.placed[node] { for _, m := range b.placed[node] {
pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m}) pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m})
} }
values, err := mmac.PlacementValues(pl) values, err := mmac.AssignmentsValues(pl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for key, raw := range values { for key, raw := range values {
b.putJSON(t, plc.PlacementBucket, key, raw) b.putJSON(t, plc.AssignmentsBucket, key, raw)
} }
} }
@@ -426,11 +436,11 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
// indexer its runtime of its own answers each first-view tool to the readers its manifest's callers lists name, at // indexer its runtime of its own answers each first-view tool to the readers its manifest's callers lists name, at
// its mark, and refuses every other account, the assigner's own included. // its mark, and refuses every other account, the assigner's own included.
firstViewsBeforeAnyNewFormState(t, b, map[string][]string{ firstViewsBeforeAnyNewFormState(t, b, map[string][]string{
"placement": {"n1.mesh-grants", "n1.node-tools", "n1.mesh-delivery-indexer"}, "assignments": {"n1.mesh-grants", "n1.node-tools", "n1.mesh-delivery-indexer"},
"seat-holders": {"n1.node-tools"}, "seat-holders": {"n1.node-tools"},
"wants": {"n1.mesh-grants"}, "wants": {"n1.mesh-grants"},
}) })
graphKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, placement.Module+"."+buckets.IndexerModule+"."+graph.State, views.UnansweredKind) graphKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, assignment.Module+"."+buckets.IndexerModule+"."+graph.State, views.UnansweredKind)
graphRaised := func() bool { graphRaised := func() bool {
e, err := b.buckets[assign.FindingsBucket].Get(b.ctx, graphKey) e, err := b.buckets[assign.FindingsBucket].Get(b.ctx, graphKey)
return err == nil && strings.Contains(string(e.Value()), buckets.IndexerModule+".graph") return err == nil && strings.Contains(string(e.Value()), buckets.IndexerModule+".graph")
@@ -492,16 +502,17 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
t.Errorf("the condition of web on neither list on g14 was not cleared once g14 got its want") t.Errorf("the condition of web on neither list on g14 was not cleared once g14 got its want")
} }
// Issue 1126 (ADR 0547 §2): the catalogue's placement reaches the indexer's state naming web on every node the // Issue 1126 (ADR 0547 §2): the catalogue's assignments reaches the indexer's state naming web on every node the
// assignments name it on. Each node then composes its own: its want names the build alone (Self, no composition), and // assignments name it on. Each node then composes its own: its want names the build alone (Self, no composition), and
// its own want says it composes its own, which the node agent reads (mesh-host's TestNats...ComposesAndAppliesItsOwn). // its own want says it composes its own, which the node agent reads (mesh-host's TestNats...ComposesAndAppliesItsOwn).
for _, node := range []string{"ace", "g14", "shanks"} { for _, node := range []string{"ace", "g14", "shanks"} {
pl, err := mmac.EncodePlacement(mma.Placement{Node: node, Modules: []mma.PlacedModule{{Module: "web"}}}) values, err := mmac.AssignmentsValues(mma.Assignments{Node: node, Modules: []mma.PlacedModule{{Module: "web"}}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
pkey, _ := mmac.LegacyKey(node) for _, k := range slices.Sorted(maps.Keys(values)) {
b.putJSON(t, plc.PlacementBucket, pkey, pl) b.putJSON(t, plc.AssignmentsBucket, k, values[k])
}
} }
if w := b.wantNow(t, "ace", "web", "ace's want once placed: the build alone, composed by the node", func(w mwc.Want) bool { return w.Self }); w.Composed != "" || w.Fp != fNew { if w := b.wantNow(t, "ace", "web", "ace's want once placed: the build alone, composed by the node", func(w mwc.Want) bool { return w.Self }); w.Composed != "" || w.Fp != fNew {
t.Errorf("ace's want once placed = %+v", w) t.Errorf("ace's want once placed = %+v", w)
@@ -521,7 +532,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
// ADR 0603 (L3, D15): one source revision changes a provider (db), its new consumer (app) and a build module (tool, whose // ADR 0603 (L3, D15): one source revision changes a provider (db), its new consumer (app) and a build module (tool, whose
// recipe moved: ace runs its first build and the second, under the agreed toolchain, is the one the source revision names). // recipe moved: ace runs its first build and the second, under the agreed toolchain, is the one the source revision names).
// The keys are written as their writers write them: the indexer's modules and manifests, the builder's builds, the // The keys are written as their writers write them: the indexer's modules and manifests, the builder's builds, the
// controller's grants, the placement seat's placement and the provisioner's provisions. The build module's want moves at // controller's grants, the assignments seat's assignments and the provisioner's provisions. The build module's want moves at
// once and the provider's with it; the consumer's waits for its provision, with the words naming it, and moves once the // once and the provider's with it; the consumer's waits for its provision, with the words naming it, and moves once the
// provisioner provides it (rule 8, ADR 0570 §4): nothing waits for a turn or an order, only for a key. // provisioner provides it (rule 8, ADR 0570 §4): nothing waits for a turn or an order, only for a key.
offer := `"provides":[{"name":"database","scope":"mesh","answered-by":"helper"}]` offer := `"provides":[{"name":"database","scope":"mesh","answered-by":"helper"}]`
@@ -1095,7 +1106,7 @@ func (s bedStore) kv(state string) jetstream.KeyValue {
if strings.Contains(state, ".") { if strings.Contains(state, ".") {
return s.b.buckets[strings.Replace(state, ".", "_", 1)] return s.b.buckets[strings.Replace(state, ".", "_", 1)]
} }
return s.b.buckets[placement.Module+"_"+state] return s.b.buckets[assignment.Module+"_"+state]
} }
func (s bedStore) get(state, key string) ([]byte, uint64, bool, error) { func (s bedStore) get(state, key string) ([]byte, uint64, bool, error) {
@@ -1125,23 +1136,23 @@ func (s bedStore) history(string, string) ([]stdio.StateRevision, error) { retur
// 1126 (ADR 0567 §2-§6), the seat mesh-module-assigner whole on the bus: a proposal moving web from the anchor to the laptop is // 1126 (ADR 0567 §2-§6), the seat mesh-module-assigner whole on the bus: a proposal moving web from the anchor to the laptop is
// proposed through the verb, the assigner finds both nodes must answer, they answer the same in their haves, the // proposed through the verb, the assigner finds both nodes must answer, they answer the same in their haves, the
// operator is asked and approves, and each key is written at a new revision while placement-changed is heard on the // operator is asked and approves, and each key is written at a new revision while assignments-changed is heard on the
// assigner's stream naming both nodes. A second proposal laid over the old revision of a key moved since is laid again, // assigner's stream naming both nodes. A second proposal laid over the old revision of a key moved since is laid again,
// and declined, writes nothing and is withdrawn. // and declined, writes nothing and is withdrawn.
func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) { func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
b := startBed(t, "") b := startBed(t, "")
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: "MOD_" + placement.Module + "_placement", if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: "MOD_" + assignment.Module + "_assignments",
Subjects: placementSubjects()}); err != nil { // the seat's four events: the views family has a stream of its own Subjects: assignmentsSubjects()}); err != nil { // the seat's four events: the views family has a stream of its own
t.Fatal(err) t.Fatal(err)
} }
heard := make(chan mma.PlacementEvent, 16) heard := make(chan mma.AssignmentsEvent, 16)
nc, err := nats.Connect(b.srv.ClientURL()) nc, err := nats.Connect(b.srv.ClientURL())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Cleanup(nc.Close) t.Cleanup(nc.Close)
if _, err := nc.Subscribe(seatdef.EventSubject(mma.Seat, mma.EventPlacementChanged), func(m *nats.Msg) { if _, err := nc.Subscribe(seatdef.EventSubject(mma.Seat, mma.EventAssignmentsChanged), func(m *nats.Msg) {
var e mma.PlacementEvent var e mma.AssignmentsEvent
if json.Unmarshal(m.Data, &e) == nil { if json.Unmarshal(m.Data, &e) == nil {
heard <- e heard <- e
} }
@@ -1155,38 +1166,34 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
b.putJSON(t, buckets.Modules, "web", []byte(`{"v":1}`)) b.putJSON(t, buckets.Modules, "web", []byte(`{"v":1}`))
b.putJSON(t, buckets.Modules, "shell", []byte(`{"v":1}`)) b.putJSON(t, buckets.Modules, "shell", []byte(`{"v":1}`))
b.putJSON(t, buckets.Modules, "mesh-enrolment", []byte(`{"v":1}`)) b.putJSON(t, buckets.Modules, "mesh-enrolment", []byte(`{"v":1}`))
// place writes a node's placement as another writer would, in its node and module keys (research 072 row 37), and // place writes a node's assignments as another writer would, in its node and module keys (research 072 row 37), and
// answers its node key's revision; legacy writes it in the form before, placement.<node>, alone. // answers its node key's revision.
place := func(node string, legacy bool, modules ...string) uint64 { place := func(node string, modules ...string) uint64 {
p := mma.Placement{Node: node, Modules: []mma.PlacedModule{}} p := mma.Assignments{Node: node, Modules: []mma.PlacedModule{}}
for _, m := range modules { for _, m := range modules {
p.Modules = append(p.Modules, mma.PlacedModule{Module: m}) p.Modules = append(p.Modules, mma.PlacedModule{Module: m})
if m == "mesh-enrolment" { // the module that holds the seat mesh-node-enroller: who may add a node is read from it if m == "mesh-enrolment" { // the module that holds the seat mesh-node-enroller: who may add a node is read from it
p.Holds = append(p.Holds, mma.Holding{Claim: "mesh-node-enroller", Scope: "mesh", Module: m}) p.Holds = append(p.Holds, mma.Holding{Claim: "mesh-node-enroller", Scope: "mesh", Module: m})
} }
} }
values, _ := mmac.PlacementValues(p) values, _ := mmac.AssignmentsValues(p)
key, _ := mmac.NodeKey(node) key, _ := mmac.NodeKey(node)
if legacy {
key, _ = mmac.LegacyKey(node)
values = map[string][]byte{key: values[key]}
}
for _, k := range slices.Sorted(maps.Keys(values)) { for _, k := range slices.Sorted(maps.Keys(values)) {
if k == key { if k == key {
continue continue
} }
if _, err := b.buckets[plc.PlacementBucket].Put(b.ctx, k, values[k]); err != nil { if _, err := b.buckets[plc.AssignmentsBucket].Put(b.ctx, k, values[k]); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
rev, err := b.buckets[plc.PlacementBucket].Put(b.ctx, key, values[key]) rev, err := b.buckets[plc.AssignmentsBucket].Put(b.ctx, key, values[key])
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return rev return rev
} }
place("anchor", true, "web") place("anchor", "web")
laptopRev := place("laptop", false, "shell") laptopRev := place("laptop", "shell")
bound := judge.AnswerBound bound := judge.AnswerBound
t.Cleanup(func() { judge.AnswerBound = bound }) t.Cleanup(func() { judge.AnswerBound = bound })
judge.AnswerBound = 2 * time.Second judge.AnswerBound = 2 * time.Second
@@ -1194,9 +1201,9 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
// The holder converts anchor, held in the form before alone, to its node and module keys once at its start. // The holder converts anchor, held in the form before alone, to its node and module keys once at its start.
var anchorRev uint64 var anchorRev uint64
for deadline := time.Now().Add(15 * time.Second); anchorRev == 0; time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(15 * time.Second); anchorRev == 0; time.Sleep(50 * time.Millisecond) {
if e, err := b.buckets[plc.PlacementBucket].Get(b.ctx, "anchor.node"); err == nil { if e, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, "anchor.node"); err == nil {
anchorRev = e.Revision() anchorRev = e.Revision()
if _, err := b.buckets[plc.PlacementBucket].Get(b.ctx, "anchor.web"); err != nil { if _, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, "anchor.web"); err != nil {
t.Fatalf("anchor converted without its module key: %v", err) t.Fatalf("anchor converted without its module key: %v", err)
} }
} }
@@ -1250,33 +1257,29 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
} }
answerAll(p) answerAll(p)
p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked }) p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
akey, _ := asks.PlacementAskKey(p.ID) akey, _ := asks.AssignmentsAskKey(p.ID)
b.answer(t, akey, assign.OptionYes) b.answer(t, akey, assign.OptionYes)
select { select {
case e := <-heard: case e := <-heard:
if e.Proposal.ID != p.ID || len(e.Nodes) != 2 || changeIn(e.Nodes[0]).Before != anchorRev || changeIn(e.Nodes[1]).Before != laptopRev || if e.Proposal.ID != p.ID || len(e.Nodes) != 2 || changeIn(e.Nodes[0]).Before != anchorRev || changeIn(e.Nodes[1]).Before != laptopRev ||
changeIn(e.Nodes[0]).After <= anchorRev || e.Warrant == nil || !slices.Equal(changeIn(e.Nodes[0]).Removed, []string{"web"}) { changeIn(e.Nodes[0]).After <= anchorRev || e.Warrant == nil || !slices.Equal(changeIn(e.Nodes[0]).Removed, []string{"web"}) {
t.Errorf("placement-changed = %+v", e) t.Errorf("assignments-changed = %+v", e)
} }
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
t.Fatal("no placement-changed heard") t.Fatal("no assignments-changed heard")
} }
for node, want := range map[string][]string{"anchor": {}, "laptop": {"shell", "web"}} { for node, want := range map[string][]string{"anchor": {}, "laptop": {"shell", "web"}} {
// The node key names the modules, each module key carries its own, and the mirror of the form before follows. // The node key names the modules, each module key carries its own.
values := map[string][]byte{} values := map[string][]byte{}
for _, key := range []string{node + ".node", "placement." + node, node + ".web", node + ".shell"} { for _, key := range []string{node + ".node", node + ".web", node + ".shell"} {
if e, err := b.buckets[plc.PlacementBucket].Get(b.ctx, key); err == nil { if e, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, key); err == nil {
values[key] = e.Value() values[key] = e.Value()
} }
} }
got, err := mmac.PlacementsOf(values) got, err := mmac.AssignmentsOf(values)
if err != nil || !slices.Equal(mmac.Assigned(got[node]), want) { if err != nil || !slices.Equal(mmac.Assigned(got[node]), want) {
t.Errorf("%s = %v, %v, want %v", node, mmac.Assigned(got[node]), err, want) t.Errorf("%s = %v, %v, want %v", node, mmac.Assigned(got[node]), err, want)
} }
mirror, _ := mmac.DecodePlacement(values["placement."+node])
if !slices.Equal(mmac.Assigned(mirror), want) {
t.Errorf("the mirror placement.%s = %v, want %v", node, mmac.Assigned(mirror), want)
}
} }
if b.hasKey(plc.ProposalsBucket, p.ID) { if b.hasKey(plc.ProposalsBucket, p.ID) {
t.Error("an applied proposal stays in the state") t.Error("an applied proposal stays in the state")
@@ -1288,13 +1291,13 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
moved := q.(proposed).Proposal moved := q.(proposed).Proposal
place("laptop", false, "shell", "web") // another writer moved the key place("laptop", "shell", "web") // another writer moved the key
relaid := proposalNow(moved.ID, "laid again", func(p mma.Proposal) bool { relaid := proposalNow(moved.ID, "laid again", func(p mma.Proposal) bool {
return strings.Contains(p.Words, "laid again") && len(p.Must) > 0 return strings.Contains(p.Words, "laid again") && len(p.Must) > 0
}) })
answerAll(relaid) answerAll(relaid)
proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked }) proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
akey, _ = asks.PlacementAskKey(moved.ID) akey, _ = asks.AssignmentsAskKey(moved.ID)
b.answer(t, akey, assign.OptionNo) b.answer(t, akey, assign.OptionNo)
for deadline := time.Now().Add(15 * time.Second); b.hasKey(plc.ProposalsBucket, moved.ID); time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(15 * time.Second); b.hasKey(plc.ProposalsBucket, moved.ID); time.Sleep(50 * time.Millisecond) {
if time.Now().After(deadline) { if time.Now().After(deadline) {
@@ -1302,7 +1305,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
} }
} }
key, _ := mmac.NodeKey("laptop") key, _ := mmac.NodeKey("laptop")
if e, _ := b.buckets[plc.PlacementBucket].Get(b.ctx, key); e == nil || !strings.Contains(string(e.Value()), "web") { if e, _ := b.buckets[plc.AssignmentsBucket].Get(b.ctx, key); e == nil || !strings.Contains(string(e.Value()), "web") {
t.Error("a declined proposal wrote its key") t.Error("a declined proposal wrote its key")
} }
@@ -1320,7 +1323,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
} }
// 774/S59 (ADR 0587 §7): a joining node is written on the enrolment's warrant with no node to answer and no ask raised, // 774/S59 (ADR 0587 §7): a joining node is written on the enrolment's warrant with no node to answer and no ask raised,
// and placement-changed carries that warrant. The assigner reads no lease (ADR 0567 §8): the enrolment's own issue waits // and assignments-changed carries that warrant. The assigner reads no lease (ADR 0567 §8): the enrolment's own issue waits
// for the lease write (ADR 0587 §4). // for the lease write (ADR 0587 §4).
for len(heard) > 0 { for len(heard) > 0 {
<-heard <-heard
@@ -1329,7 +1332,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
// now, on a node of its own. // now, on a node of its own.
hubRecord, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "hub"}}) hubRecord, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "hub"}})
b.putJSON(t, buckets.Have, "hub.node", hubRecord) b.putJSON(t, buckets.Have, "hub.node", hubRecord)
place("hub", false, "mesh-enrolment") place("hub", "mesh-enrolment")
joined, err := verbs.doPropose(map[string]any{"why": "enrolment of ace", "warrant": `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`, joined, err := verbs.doPropose(map[string]any{"why": "enrolment of ace", "warrant": `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`,
"changes": `[{"op":"add-node","node":"ace","decided":{"address":"10.9.0.2"},"keys":{"overlay":"dHVubmVs"}}]`}, "novox.mesh-enrolment") "changes": `[{"op":"add-node","node":"ace","decided":{"address":"10.9.0.2"},"keys":{"overlay":"dHVubmVs"}}]`}, "novox.mesh-enrolment")
if err != nil { if err != nil {
@@ -1338,19 +1341,19 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
select { select {
case e := <-heard: case e := <-heard:
if e.Proposal.ID != joined.(proposed).Proposal.ID || e.Warrant == nil || e.Warrant.Warrant != "enrol-ace-1@2026-10-28T20:00:00Z" || len(e.Nodes) != 1 || changeIn(e.Nodes[0]).Node != "ace" { if e.Proposal.ID != joined.(proposed).Proposal.ID || e.Warrant == nil || e.Warrant.Warrant != "enrol-ace-1@2026-10-28T20:00:00Z" || len(e.Nodes) != 1 || changeIn(e.Nodes[0]).Node != "ace" {
t.Errorf("placement-changed for the joining node = %+v", e) t.Errorf("assignments-changed for the joining node = %+v", e)
} }
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
t.Fatal("no placement-changed heard for the joining node") t.Fatal("no assignments-changed heard for the joining node")
} }
if ace, err := b.buckets[plc.PlacementBucket].Get(b.ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), "10.9.0.2") { if ace, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), "10.9.0.2") {
t.Errorf("the joining node's key = %v, %v", ace, err) t.Errorf("the joining node's key = %v, %v", ace, err)
} }
if akey, _ := asks.PlacementAskKey(joined.(proposed).Proposal.ID); b.hasKey(buckets.Asks, akey) || b.hasKey(plc.ProposalsBucket, joined.(proposed).Proposal.ID) { if akey, _ := asks.AssignmentsAskKey(joined.(proposed).Proposal.ID); b.hasKey(buckets.Asks, akey) || b.hasKey(plc.ProposalsBucket, joined.(proposed).Proposal.ID) {
t.Error("an ask was raised for the joining node, or its proposal stays open") t.Error("an ask was raised for the joining node, or its proposal stays open")
} }
// Its enrolment ends before it confirms: a remove-node on the same warrant deletes its key at once, with no ask, and // Its enrolment ends before it confirms: a remove-node on the same warrant deletes its key at once, with no ask, and
// placement-changed carries the warrant (the operator's decision of 2026-11-02 01:05). // assignments-changed carries the warrant (the operator's decision of 2026-11-02 01:05).
for len(heard) > 0 { for len(heard) > 0 {
<-heard <-heard
} }
@@ -1361,12 +1364,12 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
select { select {
case e := <-heard: case e := <-heard:
if e.Warrant == nil || len(e.Nodes) != 1 || changeIn(e.Nodes[0]).Node != "ace" { if e.Warrant == nil || len(e.Nodes) != 1 || changeIn(e.Nodes[0]).Node != "ace" {
t.Errorf("placement-changed for the removed node = %+v", e) t.Errorf("assignments-changed for the removed node = %+v", e)
} }
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
t.Fatal("no placement-changed heard for the removed node") t.Fatal("no assignments-changed heard for the removed node")
} }
if _, err := b.buckets[plc.PlacementBucket].Get(b.ctx, "ace.node"); err == nil { if _, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, "ace.node"); err == nil {
t.Error("the removed node's key stays") t.Error("the removed node's key stays")
} }
} }
@@ -1382,8 +1385,8 @@ func (b *bed) viewsHold(t *testing.T, state, key string) {
t.Fatal(err) t.Fatal(err)
} }
for deadline := time.Now().Add(20 * time.Second); ; time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(20 * time.Second); ; time.Sleep(50 * time.Millisecond) {
f := &views.Follower{Owner: placement.Module, Tool: state, State: state, Ask: func(context.Context, string) (views.Answer, error) { f := &views.Follower{Owner: assignment.Module, Tool: state, State: state, Ask: func(context.Context, string) (views.Answer, error) {
return views.FirstView(placement.Module, state, src(kv), src(marks), "", time.Now()) return views.FirstView(assignment.Module, state, src(kv), src(marks), "", time.Now())
}} }}
if err := f.Start(b.ctx); err != nil { if err := f.Start(b.ctx); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1392,7 +1395,7 @@ func (b *bed) viewsHold(t *testing.T, state, key string) {
gap := false gap := false
for seq := info.State.FirstSeq; seq > 0 && seq <= info.State.LastSeq; seq++ { for seq := info.State.FirstSeq; seq > 0 && seq <= info.State.LastSeq; seq++ {
m, err := st.GetMsg(b.ctx, seq) m, err := st.GetMsg(b.ctx, seq)
if owner, _ := views.OwnerOfEvent(m.Subject); err != nil || owner != placement.Module || views.StateOfEvent(m.Subject) != state { if owner, _ := views.OwnerOfEvent(m.Subject); err != nil || owner != assignment.Module || views.StateOfEvent(m.Subject) != state {
continue continue
} }
e, err := views.DecodeEvent(m.Data) e, err := views.DecodeEvent(m.Data)
@@ -1446,11 +1449,11 @@ func (s bedSource) Get(key string) (*views.Entry, bool, error) {
return &views.Entry{Value: e.Value(), Revision: e.Revision()}, true, nil return &views.Entry{Value: e.Value(), Revision: e.Revision()}, true, nil
} }
// placementSubjects are the subjects of the seat's four events on the assigner's stream placement. // assignmentsSubjects are the subjects of the seat's four events on the assigner's stream assignments.
func placementSubjects() []string { func assignmentsSubjects() []string {
var out []string var out []string
for _, e := range mma.Events() { for _, e := range mma.Events() {
out = append(out, process.ModuleStreamSubject(placement.Module, e)) out = append(out, process.ModuleStreamSubject(assignment.Module, e))
} }
return out return out
} }
@@ -1553,7 +1556,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
outR, outW := io.Pipe() outR, outW := io.Pipe()
t.Cleanup(func() { inW.Close(); outW.Close() }) t.Cleanup(func() { inW.Close(); outW.Close() })
go func() { go func() {
_ = stdio.ServeOn(placement.Module, viewTools(func(k string) string { return env[k] }, state, time.Now), inR, outW) _ = stdio.ServeOn(assignment.Module, viewTools(func(k string) string { return env[k] }, state, time.Now), inR, outW)
}() }()
answers := make(chan map[string]any, 16) answers := make(chan map[string]any, 16)
go func() { go func() {
@@ -1574,7 +1577,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
t.Fatal(err) t.Fatal(err)
} }
t.Cleanup(nc.Close) t.Cleanup(nc.Close)
sub, err := nc.Subscribe("mesh.mod."+placement.Module+".call.*.*", func(msg *nats.Msg) { sub, err := nc.Subscribe("mesh.mod."+assignment.Module+".call.*.*", func(msg *nats.Msg) {
one <- struct{}{} one <- struct{}{}
defer func() { <-one }() defer func() { <-one }()
tokens := strings.Split(msg.Subject, ".") tokens := strings.Split(msg.Subject, ".")
@@ -1606,7 +1609,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
ask := func(user, tool string) (views.Answer, error) { ask := func(user, tool string) (views.Answer, error) {
return views.AskVia(func(key string, body any) (json.RawMessage, error) { return views.AskVia(func(key string, body any) (json.RawMessage, error) {
data, _ := json.Marshal(body) data, _ := json.Marshal(body)
msg, err := nc.Request("mesh.mod."+placement.Module+".call."+tool+"."+strings.ReplaceAll(user, ".", "~"), data, 5*time.Second) msg, err := nc.Request("mesh.mod."+assignment.Module+".call."+tool+"."+strings.ReplaceAll(user, ".", "~"), data, 5*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1622,7 +1625,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
t.Errorf("%s's first view of %s before any new-form state: %v", user, tool, err) t.Errorf("%s's first view of %s before any new-form state: %v", user, tool, err)
case slices.Contains(allowed, user): case slices.Contains(allowed, user):
keys, _ := b.buckets[viewedStates[tool]].Keys(b.ctx) keys, _ := b.buckets[viewedStates[tool]].Keys(b.ctx)
if a.Owner != placement.Module || a.State != tool || len(a.Values) != len(keys) { if a.Owner != assignment.Module || a.State != tool || len(a.Values) != len(keys) {
t.Errorf("%s's first view of %s = %+v, the state holds %v", user, tool, a, keys) t.Errorf("%s's first view of %s = %+v, the state holds %v", user, tool, a, keys)
} }
case err == nil || !strings.Contains(err.Error(), "refused"): case err == nil || !strings.Contains(err.Error(), "refused"):
@@ -1632,7 +1635,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
} }
} }
// changeIn reads one entry of a placement-changed event's nodes. // changeIn reads one entry of assignments-changed event's nodes.
func changeIn(body map[string]any) plc.Change { func changeIn(body map[string]any) plc.Change {
raw, _ := json.Marshal(body) raw, _ := json.Marshal(body)
var c plc.Change var c plc.Change
@@ -12,8 +12,8 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"os" "os"
"os/signal" "os/signal"
"slices" "slices"
@@ -23,10 +23,10 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
@@ -34,7 +34,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
func main() { func main() {
@@ -75,29 +75,29 @@ var readFamilies = []string{buckets.ComposedRead}
// ownBuckets are the assigner's own states, as its manifest declares them: the seat mesh-module-assigner's three among them // ownBuckets are the assigner's own states, as its manifest declares them: the seat mesh-module-assigner's three among them
// (ADR 0567 §1), named after the assigner as every module state is. // (ADR 0567 §1), named after the assigner as every module state is.
var ownBuckets = []string{buckets.Asks, buckets.Differences, var ownBuckets = []string{buckets.Asks, buckets.Differences,
buckets.Puts, buckets.Wants, plc.PlacementBucket, plc.ProposalsBucket, buckets.Puts, buckets.Wants, plc.AssignmentsBucket, plc.ProposalsBucket,
plc.CandidatesBucket, plc.PulsesBucket, assign.FindingsBucket, assign.PoliciesBucket, plc.SeatHoldersBucket, plc.CandidatesBucket, plc.PulsesBucket, assign.FindingsBucket, assign.PoliciesBucket, plc.SeatHoldersBucket,
engineReadsBucket} engineReadsBucket, retiredBucket}
// engineReadsState is the assigner's state of what each node's engine states it reads (ADR 0618 §3), one key `<node>`, as // engineReadsState is the assigner's state of what each node's engine states it reads (ADR 0618 §3), one key `<node>`, as
// the verb manifest-reads last answered it or the fallback read it: what assigner_engine_reads lists and asks again. // the verb manifest-reads last answered it or the fallback read it: what assigner_engine_reads lists and asks again.
const engineReadsState = "engine-reads" const engineReadsState = "engine-reads"
// engineReadsBucket is that state's bucket. // engineReadsBucket is that state's bucket.
const engineReadsBucket = placement.Module + "_" + engineReadsState const engineReadsBucket = assignment.Module + "_" + engineReadsState
// seatBuckets are the seat's states, new with it: until the controller has made them from the manifest and the bus grants // seatBuckets are the seat's states, new with it: until the controller has made them from the manifest and the bus grants
// them, the assigner starts without them and binds each once the bus says it was created (issue 1790's kind). // them, the assigner starts without them and binds each once the bus says it was created (issue 1790's kind).
var seatBuckets = []string{plc.PlacementBucket, plc.ProposalsBucket, plc.CandidatesBucket, var seatBuckets = []string{plc.AssignmentsBucket, plc.ProposalsBucket, plc.CandidatesBucket,
plc.PulsesBucket} plc.PulsesBucket}
// keptStates is what the assigner keeps and who may read it (ADR 0495 §2-§3): its wants by the watchman and the view // keptStates is what the assigner keeps and who may read it (ADR 0495 §2-§3): its wants by the watchman and the view
// (and the node agent from release 6), its differences by the view, its asks by the view // (and the node agent from release 6), its differences by the view, its asks by the view
// and the teller, its puts by whoever follows one (ADR 0546), and the seat mesh-module-assigner's placement, proposals and // and the teller, its puts by whoever follows one (ADR 0546), and the seat mesh-module-assigner's assignments, proposals and
// candidates by the node agents, the grants part, mesh-config and the view (ADR 0567 §9), and its placement by // candidates by the node agents, the grants part, mesh-config and the view (ADR 0567 §9), and its assignments by
// the builder, the checker and the indexer, through mesh-sdk's toolchain gate (issue 774, ADR 0549 §3). // the builder, the checker and the indexer, through mesh-sdk's toolchain gate (issue 774, ADR 0549 §3).
var keptStates = []announce.State{ var keptStates = []announce.State{
{Name: mma.StatePlacement, Readable: true}, {Name: mma.StateAssignments, Readable: true},
{Name: mma.StateProposals, Readable: true}, {Name: mma.StateProposals, Readable: true},
{Name: mma.StateCandidates, Readable: true}, {Name: mma.StateCandidates, Readable: true},
{Name: mma.StatePulses, Readable: true}, {Name: mma.StatePulses, Readable: true},
@@ -111,6 +111,7 @@ var keptStates = []announce.State{
{Name: views.MarksState, Readable: true}, // what its views family said each state up to (views.go) {Name: views.MarksState, Readable: true}, // what its views family said each state up to (views.go)
{Name: views.StatusState, Readable: true}, // its first views of the indexer's states (views.go) {Name: views.StatusState, Readable: true}, // its first views of the indexer's states (views.go)
{Name: engineReadsState, Readable: true}, {Name: engineReadsState, Readable: true},
{Name: retiredState}, // read only by the copy into assignments (migrate.go)
} }
// kvSet is every bucket the assigner opened, by name. // kvSet is every bucket the assigner opened, by name.
@@ -129,7 +130,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
continue continue
} }
if slices.Contains(seatBuckets, name) && err != nil { if slices.Contains(seatBuckets, name) && err != nil {
// The seat's states come in the same delivery as this assigner (ADR 0567 §1): without placement no module is placed. // The seat's states come in the same delivery as this assigner (ADR 0567 §1): without assignments no module is placed.
logf("the bucket %s cannot be opened (%v): it is bound once the bus says it was made", name, err) logf("the bucket %s cannot be opened (%v): it is bound once the bus says it was made", name, err)
continue continue
} }
@@ -169,6 +170,12 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
logf("the bucket %s cannot be opened (%v): what each node's engine reads is kept in memory only until it starts again", name, err) logf("the bucket %s cannot be opened (%v): what each node's engine reads is kept in memory only until it starts again", name, err)
continue continue
} }
if name == retiredBucket && err != nil {
// The retired state is read only for the copy into `assignments` (migrate.go): a mesh made after the switch has
// none, and nothing is copied.
logf("the bucket %s cannot be opened (%v): nothing is copied from it", name, err)
continue
}
if name == buckets.Grants && err != nil { if name == buckets.Grants && err != nil {
// The grants part's grants (ADR 0571 clause 9): without them no module counts as granted until it is bound. // The grants part's grants (ADR 0571 clause 9): without them no module counts as granted until it is bound.
logf("the bucket %s cannot be opened (%v): no grant is read until it is bound once the bus says it was made", name, err) logf("the bucket %s cannot be opened (%v): no grant is read until it is bound once the bus says it was made", name, err)
@@ -187,7 +194,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
func startsWithout(name string) bool { func startsWithout(name string) bool {
switch name { switch name {
case plc.SeatHoldersBucket, buckets.Provisions, assign.PoliciesBucket, assign.FindingsBucket, case plc.SeatHoldersBucket, buckets.Provisions, assign.PoliciesBucket, assign.FindingsBucket,
buckets.Puts, buckets.Source, buckets.Merges, engineReadsBucket: buckets.Puts, buckets.Source, buckets.Merges, engineReadsBucket, retiredBucket:
return true return true
} }
return slices.Contains(seatBuckets, name) || name == buckets.Grants return slices.Contains(seatBuckets, name) || name == buckets.Grants
@@ -271,7 +278,7 @@ var watched = []string{
buckets.Grants, buckets.Grants,
buckets.Answers, buckets.Answers,
buckets.Builds, buckets.Builds,
plc.PlacementBucket, plc.AssignmentsBucket,
plc.ProposalsBucket, plc.ProposalsBucket,
plc.CandidatesBucket, plc.CandidatesBucket,
plc.PulsesBucket, plc.PulsesBucket,
@@ -419,11 +426,11 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
name string name string
kv jetstream.KeyValue kv jetstream.KeyValue
} }
placementAppeared := make(chan appeared, len(seatBuckets)+2) assignmentsAppeared := make(chan appeared, len(seatBuckets)+2)
for _, name := range seatBuckets { for _, name := range seatBuckets {
if kvs[name] == nil && js != nil { if kvs[name] == nil && js != nil {
process.AwaitBucket(ctx, js, nc, name, "the seat mesh-module-assigner waits for it", process.AwaitBucket(ctx, js, nc, name, "the seat mesh-module-assigner waits for it",
logger.Printf).Ready(func(kv jetstream.KeyValue) { placementAppeared <- appeared{name, kv} }) logger.Printf).Ready(func(kv jetstream.KeyValue) { assignmentsAppeared <- appeared{name, kv} })
} }
} }
// And the two that choose where grants are read from, with the same delivery as the grants part that writes one. // And the two that choose where grants are read from, with the same delivery as the grants part that writes one.
@@ -431,16 +438,20 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
if kvs[name] == nil && js != nil { if kvs[name] == nil && js != nil {
name := name name := name
process.AwaitBucket(ctx, js, nc, name, "until then the bucket is not read", process.AwaitBucket(ctx, js, nc, name, "until then the bucket is not read",
logger.Printf).Ready(func(kv jetstream.KeyValue) { placementAppeared <- appeared{name, kv} }) logger.Printf).Ready(func(kv jetstream.KeyValue) { assignmentsAppeared <- appeared{name, kv} })
} }
} }
takePlacement := func(a appeared) error { takeAssignments := func(a appeared) error {
if kvs[a.name] != nil { if kvs[a.name] != nil {
return nil // bound already, by the other of its two events return nil // bound already, by the other of its two events
} }
kvs[a.name] = a.kv kvs[a.name] = a.kv
logger.Printf("the bucket %s is open and watched", a.name) logger.Printf("the bucket %s is open and watched", a.name)
return watch(a.name) if err := watch(a.name); err != nil {
return err
}
wake() // a state made empty brings no change of its own: the pass copies the retired state into it (migrate.go)
return nil
} }
for range started { for range started {
select { select {
@@ -476,8 +487,8 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
case err := <-errs: case err := <-errs:
return err return err
case <-settled: case <-settled:
case kv := <-placementAppeared: case kv := <-assignmentsAppeared:
if err := takePlacement(kv); err != nil { if err := takeAssignments(kv); err != nil {
return err return err
} }
case <-dirty: case <-dirty:
@@ -491,7 +502,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
continue continue
} }
if kv, err := js.KeyValue(ctx, name); err == nil { if kv, err := js.KeyValue(ctx, name); err == nil {
if err := takePlacement(appeared{name, kv}); err != nil { if err := takeAssignments(appeared{name, kv}); err != nil {
return err return err
} }
continue continue
@@ -511,7 +522,7 @@ var PutsReopenEvery = time.Minute
// (ADR 0437 clause 9). // (ADR 0437 clause 9).
func sayVia(ctx context.Context, js jetstream.JetStream) func(oc.Condition) error { func sayVia(ctx context.Context, js jetstream.JetStream) func(oc.Condition) error {
return func(c oc.Condition) error { return func(c oc.Condition) error {
subject, err := occ.ConditionSubject(placement.Module) subject, err := occ.ConditionSubject(assignment.Module)
if err != nil { if err != nil {
return err return err
} }
@@ -528,7 +539,7 @@ func sayVia(ctx context.Context, js jetstream.JetStream) func(oc.Condition) erro
// consumer on EVENTS, and asks the node it names again; the hub also hands the indexer's views to the followers of // consumer on EVENTS, and asks the node it names again; the hub also hands the indexer's views to the followers of
// indexerViews, and starts once both are registered. // indexerViews, and starts once both are registered.
func hearReadsChanged(ctx context.Context, js jetstream.JetStream, hub *process.EventHub, reads *enginereads.Keeper, findings assign.Findings, logf func(string, ...any)) { func hearReadsChanged(ctx context.Context, js jetstream.JetStream, hub *process.EventHub, reads *enginereads.Keeper, findings assign.Findings, logf func(string, ...any)) {
account := process.Account{Node: thisNode, Module: placement.Module} account := process.Account{Node: thisNode, Module: assignment.Module}
w := notHeardWriter(findings, logf) w := notHeardWriter(findings, logf)
handle := func(m jetstream.Msg) { handle := func(m jetstream.Msg) {
if err := m.Ack(); err != nil { if err := m.Ack(); err != nil {
@@ -2,8 +2,8 @@ package main
import ( import (
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
plc "mesh-delivery-assigner/internal/placement"
"context" "context"
"errors" "errors"
@@ -20,9 +20,9 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
) )
@@ -112,12 +112,12 @@ func TestOpensWhatItsManifestNames(t *testing.T) {
for _, name := range append(append([]string{}, readBuckets...), ownBuckets...) { for _, name := range append(append([]string{}, readBuckets...), ownBuckets...) {
kvs[name] = &watchKV{memKV: &memKV{values: map[string][]byte{}}} kvs[name] = &watchKV{memKV: &memKV{values: map[string][]byte{}}}
} }
placed, err := mmac.PlacementValues(mma.Placement{Node: "ace"}) placed, err := mmac.AssignmentsValues(mma.Assignments{Node: "ace"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for key, raw := range placed { for key, raw := range placed {
kv := kvs[plc.PlacementBucket].(*watchKV) kv := kvs[plc.AssignmentsBucket].(*watchKV)
kv.entries = append(kv.entries, kvEntry{key: key, value: raw}) kv.entries = append(kv.entries, kvEntry{key: key, value: raw})
} }
kvs[buckets.Puts].(*watchKV).closed = true kvs[buckets.Puts].(*watchKV).closed = true
@@ -192,3 +192,8 @@ func (e kvEntry) Value() []byte { return e.value }
func (e kvEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut } func (e kvEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut }
func (e kvEntry) Revision() uint64 { return 1 } func (e kvEntry) Revision() uint64 { return 1 }
func (e kvEntry) Created() time.Time { return time.Time{} } func (e kvEntry) Created() time.Time { return time.Time{} }
// Keys lists nothing: the retired state this watch stands for holds no key.
func (w *watchKV) Keys(context.Context, ...jetstream.WatchOpt) ([]string, error) {
return nil, jetstream.ErrNoKeysFound
}
@@ -0,0 +1,126 @@
package main
import (
"context"
"errors"
"slices"
"strings"
"github.com/nats-io/nats.go/jetstream"
plc "mesh-delivery-assigner/internal/assignment"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
)
// The state this seat's assignments were kept in before ADR 0641 gave it its name. The assigner reads it here and nowhere
// else: at its start it copies each key into `assignments` (copyRetired) and keeps no reader of it afterwards. The
// manifest still declares the state, so the assigner's account may read it for the copy; a later release drops it.
const (
retiredState = "placement"
retiredBucket = plc.Module + "_" + retiredState
)
// CopiedEvent is the module's event saying the copy was made, once: mesh.mod.mesh-delivery-assigner.event.assignments-copied.
const CopiedEvent = "assignments-copied"
// copiedSaid is what the event carries: the keys copied, those already in `assignments` (kept as they are), and the keys of
// the form `placement.<node>` that were dropped (their node key holds the value).
type copiedSaid struct {
Copied int `json:"copied"`
Present int `json:"present"`
Dropped int `json:"dropped"`
Skipped int `json:"skipped"`
}
// copyRetired is the holder's switch from the retired state to `assignments`, with no gap: for each key of the retired state
// in the form `<node>.node` or `<node>.<module>` it copies the raw value into `assignments` when the key is missing there,
// the module keys first and the node keys, which commit a node, last. A key copied or already there is then deleted from the
// retired state, so a restart never brings back what `assignments` has dropped since. The keys of the form
// `placement.<node>` are dropped, and a key that fits no form is left and counted. It answers whether the seat may go on: a
// copy that failed is tried again at the next pass.
func (p *passer) copyRetired(ctx context.Context) bool {
if p.copied {
return true
}
old, next := p.kvs[retiredBucket], p.kvs[plc.AssignmentsBucket]
if next == nil {
return false
}
if old == nil {
p.logf("assignments: the retired state %s cannot be read, so nothing is copied from it", retiredBucket)
p.copied = true
return true
}
names, err := old.Keys(ctx)
if err != nil && !errors.Is(err, jetstream.ErrNoKeysFound) {
p.logf("assignments: the keys of %s cannot be listed, and are tried again at the next pass: %v", retiredBucket, err)
return false
}
// A node's module keys before its node key: the node key is the commit the readers wait for.
slices.SortStableFunc(names, func(a, b string) int {
return strings.Compare(sortForm(a), sortForm(b))
})
said := copiedSaid{}
for _, key := range names {
_, _, kind, perr := mmac.ParseKey(key)
switch {
case strings.HasPrefix(key, retiredState+"."):
// The node named placement could not exist before, so this is the form that held a whole node in one key.
said.Dropped++
case perr != nil || (kind != mmac.NodeKind && kind != mmac.ModuleKind):
said.Skipped++
continue
default:
entry, err := old.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
continue
}
if err != nil {
p.logf("assignments: %s cannot be read, and the copy is tried again at the next pass: %v", key, err)
return false
}
if entry.Operation() != jetstream.KeyValuePut {
continue
}
rev, err := next.Create(ctx, key, entry.Value())
switch {
case errors.Is(err, jetstream.ErrKeyExists):
said.Present++
case err != nil:
p.logf("assignments: %s is not copied, and the copy is tried again at the next pass: %v", key, err)
return false
default:
said.Copied++
p.w.putEntry(plc.AssignmentsBucket, key, entry.Value(), rev, p.clock())
}
}
if err := old.Delete(ctx, key); err != nil {
p.logf("assignments: %s is not deleted from %s after its copy, and the copy is tried again at the next pass: %v", key, retiredBucket, err)
return false
}
}
p.copied = true
if said == (copiedSaid{}) {
return true
}
p.logf("assignments: copied %d keys of %s into %s (%d already there kept as they are, %d of the form placement.<node> dropped, %d fitting no form left)",
said.Copied, retiredBucket, plc.AssignmentsBucket, said.Present, said.Dropped, said.Skipped)
if said.Copied+said.Present+said.Dropped == 0 {
return true // only keys left behind: the line above says them at each start, and the event was said with the copy
}
if p.seat != nil {
if err := p.seat.bus.Emit(CopiedEvent, said); err != nil {
p.logf("assignments: the copy is not said as an event: %v", err)
}
}
return true
}
// sortForm orders `<node>.<module>` keys before `<node>.node`.
func sortForm(key string) string {
if node, ok := strings.CutSuffix(key, ".node"); ok {
return node + "\x7f"
}
return key
}
@@ -12,7 +12,7 @@ import (
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
@@ -26,7 +26,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// passer makes the assigner's passes: from what the world holds now, it writes each key whose value differs from the // passer makes the assigner's passes: from what the world holds now, it writes each key whose value differs from the
@@ -74,8 +74,8 @@ type passer struct {
seat *seatActs seat *seatActs
publish publisher publish publisher
saidProposed map[string]bool saidProposed map[string]bool
// convertedAll says every node in the form before was converted to its node and module keys (convertPlacement). // copied says the retired state was copied into `assignments` by this process, or found empty (copyRetired).
convertedAll bool copied bool
// saidWaits is the last said of the seat's wait for its states (sayWaits). // saidWaits is the last said of the seat's wait for its states (sayWaits).
saidWaits string saidWaits string
intended map[string]map[string]map[string]bool intended map[string]map[string]map[string]bool
@@ -123,7 +123,7 @@ func (p *passer) clock() time.Time {
} }
func (p *passer) pass(ctx context.Context) { func (p *passer) pass(ctx context.Context) {
p.placementSeat(ctx) p.assignmentsSeat(ctx)
p.wants(ctx) p.wants(ctx)
p.puts(ctx) p.puts(ctx)
p.nodeWants(ctx) p.nodeWants(ctx)
@@ -1071,7 +1071,7 @@ func (p *passer) provisionsMissing(ctx context.Context, missing map[string]bool)
} }
// unplacedFindings writes, from the lease write, the finding `machine.<node>.unplaced` for each node that runs but no // unplacedFindings writes, from the lease write, the finding `machine.<node>.unplaced` for each node that runs but no
// placement names, which gets no want (ADR 0571 §6), and deletes it once the node is placed. // assignments names, which gets no want (ADR 0571 §6), and deletes it once the node is placed.
func (p *passer) unplacedFindings() { func (p *passer) unplacedFindings() {
if p.findings.Put == nil { if p.findings.Put == nil {
return return
@@ -1106,20 +1106,20 @@ func (p *passer) unplacedFindings() {
} }
} }
// seatHolders writes the seat mesh-module-assigner's state seat-holders from every node's placement (ADR 0571 §4): one key per // seatHolders writes the seat mesh-module-assigner's state seat-holders from every node's assignments (ADR 0571 §4): one key per
// seat held once, `<seat>`, and one per node of a node seat, `<seat>.<node>`, written only where it differs, and a key no // seat held once, `<seat>`, and one per node of a node seat, `<seat>.<node>`, written only where it differs, and a key no
// placement holds any more deleted. A pass follows each placement change and each manifest the indexer takes in, so no // assignments holds any more deleted. A pass follows each assignments change and each manifest the indexer takes in, so no
// timer writes it. // timer writes it.
func (p *passer) seatHolders(ctx context.Context) { func (p *passer) seatHolders(ctx context.Context) {
kv := p.kvs[plc.SeatHoldersBucket] kv := p.kvs[plc.SeatHoldersBucket]
if kv == nil { if kv == nil {
return return
} }
placements := p.w.placements() assignments := p.w.assignments()
if len(placements) == 0 { if len(assignments) == 0 {
return // no placement read yet: nothing is known of who holds what, and nothing is deleted return // no assignments read yet: nothing is known of who holds what, and nothing is deleted
} }
want := plc.SeatHoldersOf(placements) want := plc.SeatHoldersOf(assignments)
have := p.w.read(plc.SeatHoldersBucket) have := p.w.read(plc.SeatHoldersBucket)
for key, sh := range want { for key, sh := range want {
value, err := json.Marshal(sh) value, err := json.Marshal(sh)
@@ -15,9 +15,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
wdc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" wdc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
mdseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" mdseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
@@ -26,10 +26,10 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/busretry" "git.novox.be/novox/mesh-sdk/go/busretry"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -38,7 +38,7 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// memKV is the part of a bucket a pass writes: puts and deletes. // memKV is the part of a bucket a pass writes: puts and deletes.
@@ -70,20 +70,20 @@ func newPasser(kvs kvSet, at time.Time) *passer {
now: func() time.Time { return at }} now: func() time.Time { return at }}
} }
// placeOn puts the seat's placement of node, naming the modules, into the world: one node key and a key per module // placeOn puts the seat's assignments of node, naming the modules, into the world: one node key and a key per module
// (research 072 row 37). // (research 072 row 37).
func placeOn(t *testing.T, w *world, node string, modules ...string) { func placeOn(t *testing.T, w *world, node string, modules ...string) {
t.Helper() t.Helper()
pl := mma.Placement{Node: node} pl := mma.Assignments{Node: node}
for _, m := range modules { for _, m := range modules {
pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m}) pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m})
} }
values, err := mmac.PlacementValues(pl) values, err := mmac.AssignmentsValues(pl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for key, raw := range values { for key, raw := range values {
w.put(plc.PlacementBucket, key, raw) w.put(plc.AssignmentsBucket, key, raw)
} }
} }
@@ -491,7 +491,7 @@ func putOfTheBus(t *testing.T) {
// the journal, so it is loud with no operator channel. // the journal, so it is loud with no operator channel.
func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) {
// A pair only one source names is a condition of the same kind (issue 1126): folded here to hold the count. // A pair only one source names is a condition of the same kind (issue 1126): folded here to hold the count.
aPlacementAloneNamesWhereModulesRun(t) aAssignmentsAloneNamesWhereModulesRun(t)
// The verb edges a want waits on come from the graph and what runs (ADR 0614 §2, §3): folded here to hold the count. // The verb edges a want waits on come from the graph and what runs (ADR 0614 §2, §3): folded here to hold the count.
aWantWaitsOnTheVerbEdgesTheGraphKeeps(t) aWantWaitsOnTheVerbEdgesTheGraphKeeps(t)
t.Run("gathers", testThePassGathersTheModulesOnNeitherList) t.Run("gathers", testThePassGathersTheModulesOnNeitherList)
@@ -972,18 +972,18 @@ func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) {
} }
} }
// 774/S26/AC1 (ADR 0549 §5, ADR 0567 §8): the seat's placement alone names where modules run: no controller state is read, so a // 774/S26/AC1 (ADR 0549 §5, ADR 0567 §8): the seat's assignments alone names where modules run: no controller state is read, so a
// module in an assignment only is no module of its node, and before any placement is read nothing is placed. // module in an assignment only is no module of its node, and before any assignments is read nothing is placed.
func aPlacementAloneNamesWhereModulesRun(t *testing.T) { func aAssignmentsAloneNamesWhereModulesRun(t *testing.T) {
p := newPasser(kvSet{}, time.Date(2026, 10, 26, 18, 0, 0, 0, time.UTC)) p := newPasser(kvSet{}, time.Date(2026, 10, 26, 18, 0, 0, 0, time.UTC))
p.w.put("mesh-controller_assignments", "g14.b", []byte(`{"node":"g14","module":"b","build":"b-1"}`)) // read no more p.w.put("mesh-controller_assignments", "g14.b", []byte(`{"node":"g14","module":"b","build":"b-1"}`)) // read no more
if got := p.w.assigned(); len(got) != 0 { if got := p.w.assigned(); len(got) != 0 {
t.Fatalf("before any placement: assigned %v", got) t.Fatalf("before any assignments: assigned %v", got)
} }
placeOn(t, p.w, "g14", "c", "a") placeOn(t, p.w, "g14", "c", "a")
placeOn(t, p.w, "n2", "d") placeOn(t, p.w, "n2", "d")
if got := p.w.assigned(); !slices.Equal(got["g14"], []string{"a", "c"}) || !slices.Equal(got["n2"], []string{"d"}) || len(got) != 2 { if got := p.w.assigned(); !slices.Equal(got["g14"], []string{"a", "c"}) || !slices.Equal(got["n2"], []string{"d"}) || len(got) != 2 {
t.Errorf("the placement alone: assigned %v", got) t.Errorf("the assignments alone: assigned %v", got)
} }
} }
@@ -1000,8 +1000,8 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) {
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpTest, Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath}) mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpTest, Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath})
put(buckets.Modules, "web", mod) // the trunk's build is not done: rule 1 holds g14 at fpOld put(buckets.Modules, "web", mod) // the trunk's build is not done: rule 1 holds g14 at fpOld
placeOn(t, p.w, "g14", "web") placeOn(t, p.w, "g14", "web")
pl, _ := mmac.EncodePlacement(mma.Placement{Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}}) pl, _ := mmac.EncodeAssignments(mma.Assignments{Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
put(plc.PlacementBucket, "placement.g14", pl) put(plc.AssignmentsBucket, "assignments.g14", pl)
on, _ := presenceValue(true) on, _ := presenceValue(true)
put(buckets.Presence, "g14", on) put(buckets.Presence, "g14", on)
have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Composed: was, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Composed: was, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
@@ -1076,19 +1076,19 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) {
} }
} }
// 774/S25/AC1 (ADR 0567 §8): an approved placement change is written and followed with its warrant recorded and // 774/S25/AC1 (ADR 0567 §8): an approved assignments change is written and followed with its warrant recorded and
// placement-changed said, and no verb of the controller is called; the refused-call conditions a previous process raised are // assignments-changed said, and no verb of the controller is called; the refused-call conditions a previous process raised are
// cleared once, and said again at a later pass while the findings cannot be read. // cleared once, and said again at a later pass while the findings cannot be read.
func TestAnApprovedPlacementCallsNoControllerVerb(t *testing.T) { func TestAnApprovedAssignmentsCallsNoControllerVerb(t *testing.T) {
p := newPasser(kvSet{}, time.Date(2026, 11, 2, 9, 0, 0, 0, time.UTC)) p := newPasser(kvSet{}, time.Date(2026, 11, 2, 9, 0, 0, 0, time.UTC))
bus := &fakeBus{calls: make(chan string, 8)} bus := &fakeBus{calls: make(chan string, 8)}
p.seat = newSeatActs(bus, func() {}, func(string, ...any) {}) p.seat = newSeatActs(bus, func() {}, func(string, ...any) {})
var said []string var said []string
p.publish = func(event string, _ any, _ string) error { said = append(said, event); return nil } p.publish = func(event string, _ any, _ string) error { said = append(said, event); return nil }
prop := mma.Proposal{ID: "p-one", By: "operator@laptop", Why: "move web"} prop := mma.Proposal{ID: "p-one", By: "operator@laptop", Why: "move web"}
change := plc.ChangeOf(mma.Placement{Node: "anchor"}, mma.Placement{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}) change := plc.ChangeOf(mma.Assignments{Node: "anchor"}, mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}})
p.followWrite(prop, mdc.Answer{}, []plc.Change{change}) p.followWrite(prop, mdc.Answer{}, []plc.Change{change})
if len(bus.calls) != 0 || !slices.Equal(said, []string{mma.EventPlacementChanged}) { if len(bus.calls) != 0 || !slices.Equal(said, []string{mma.EventAssignmentsChanged}) {
t.Fatalf("calls = %d, said = %v", len(bus.calls), said) t.Fatalf("calls = %d, said = %v", len(bus.calls), said)
} }
@@ -13,8 +13,8 @@ import (
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
@@ -6,8 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"maps" "maps"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"reflect" "reflect"
"slices" "slices"
"sort" "sort"
@@ -24,24 +24,24 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/seatdef"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/judge" "mesh-delivery-assigner/internal/judge"
plc "mesh-delivery-assigner/internal/placement"
) )
// The seat mesh-module-assigner's holder in the assigner's process (novox/hq ADR 0567 §2-§8): each pass follows every open // The seat mesh-module-assigner's holder in the assigner's process (novox/hq ADR 0567 §2-§8): each pass follows every open
// proposal one step, from what the world holds. A proposal the verbs wrote is said (placement-proposed); the nodes it // proposal one step, from what the world holds. A proposal the verbs wrote is said (assignments-proposed); the nodes it
// must hear from are found by composing every node with and without it, and those its key does not change get a reached // must hear from are found by composing every node with and without it, and those its key does not change get a reached
// candidate; their answers are judged (placement-judged) and, on a passing verdict, the operator is asked to approve it; // candidate; their answers are judged (assignments-judged) and, on a passing verdict, the operator is asked to approve it;
// on the warrant each changed key is written by compare-and-set at the revision it was laid over (placement-changed), // on the warrant each changed key is written by compare-and-set at the revision it was laid over (assignments-changed),
// and the controller's assignments are kept in step through its verbs until the lease write. A key moved meanwhile // and the controller's assignments are kept in step through its verbs until the lease write. A key moved meanwhile
// lays the proposal again; a decline, an expiry or a withdrawal ends it (placement-withdrawn). Every event is said on the // lays the proposal again; a decline, an expiry or a withdrawal ends it (assignments-withdrawn). Every event is said on the
// assigner's own stream, so only this process says them. Nothing polls: the wake at an answer's bound is the one wake. // assigner's own stream, so only this process says them. Nothing polls: the wake at an answer's bound is the one wake.
// publisher says one of the seat's events on the assigner's stream, under a message id the bus keeps one of. // publisher says one of the seat's events on the assigner's stream, under a message id the bus keeps one of.
@@ -58,7 +58,7 @@ func seatPublisher(ctx context.Context, js jetstream.JetStream, node string) pub
} }
msg := nats.NewMsg(seatdef.EventSubject(mma.Seat, event)) msg := nats.NewMsg(seatdef.EventSubject(mma.Seat, event))
msg.Header.Set("x-event-id", id) msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", placement.Module) msg.Header.Set("x-source", assignment.Module)
msg.Header.Set("x-node", node) msg.Header.Set("x-node", node)
msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano)) msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json") msg.Header.Set("content-type", "application/json")
@@ -69,7 +69,7 @@ func seatPublisher(ctx context.Context, js jetstream.JetStream, node string) pub
} }
// seatActs keeps the seat's acts while this process runs: the warrant recorded in the hand-act log. The seat calls no verb of // seatActs keeps the seat's acts while this process runs: the warrant recorded in the hand-act log. The seat calls no verb of
// the controller: placement is the one source (ADR 0567 §8, issue 774). // the controller: assignments is the one source (ADR 0567 §8, issue 774).
type seatActs struct { type seatActs struct {
bus caller bus caller
wake func() wake func()
@@ -82,7 +82,7 @@ func newSeatActs(bus caller, wake func(), logf func(string, ...any)) *seatActs {
return &seatActs{bus: bus, wake: wake, logf: logf, rec: meshhandactsclient.Recorder{Ask: bus.Call}} return &seatActs{bus: bus, wake: wake, logf: logf, rec: meshhandactsclient.Recorder{Ask: bus.Call}}
} }
// record records once, in the hand-act log, that placement was written on the operator's warrant (§5): the router's // record records once, in the hand-act log, that assignments was written on the operator's warrant (§5): the router's
// record of the teller's ask says who chose and how (ADR 0274). The log is the seat mesh-hand-acts' from its delivery, and // record of the teller's ask says who chose and how (ADR 0274). The log is the seat mesh-hand-acts' from its delivery, and
// the controller's while that seat has no holder (ADR 0565 §11, meshhandactsclient.Recorder). // the controller's while that seat has no holder (ADR 0565 §11, meshhandactsclient.Recorder).
func (a *seatActs) record(warrant string) { func (a *seatActs) record(warrant string) {
@@ -92,9 +92,9 @@ func (a *seatActs) record(warrant string) {
ask, _, _ := strings.Cut(warrant, "@") ask, _, _ := strings.Cut(warrant, "@")
go func() { go func() {
if taken, err := a.rec.RecordWarrant(buckets.TellerModule, ask); err != nil { if taken, err := a.rec.RecordWarrant(buckets.TellerModule, ask); err != nil {
a.logf("the placement's warrant %s is not recorded in the hand-act log: %v", ask, err) a.logf("the assignments's warrant %s is not recorded in the hand-act log: %v", ask, err)
} else { } else {
a.logf("the placement's warrant %s is recorded in the log of %s", ask, taken.Log) a.logf("the assignments's warrant %s is recorded in the log of %s", ask, taken.Log)
} }
}() }()
} }
@@ -147,10 +147,9 @@ func (p *passer) sayWaits(unbound []string) {
p.said[c.Key], p.saidWaits = true, state p.said[c.Key], p.saidWaits = true, state
} }
// seatPlacements is every node's placement and the revision of its node key (its legacy key's for a node not yet // seatAssignments is every node's assignments and the revision of its node key, as the watch holds them: the revision a proposal is laid over (§2, research 072 row 37).
// converted), as the watch holds them: the revision a proposal is laid over (§2, research 072 row 37). func (p *passer) seatAssignments() (map[string]mma.Assignments, map[string]uint64) {
func (p *passer) seatPlacements() (map[string]mma.Placement, map[string]uint64) { return p.w.assignmentKeys.AllRevisions()
return p.w.placement.AllRevisions()
} }
// candidatesOf is a proposal's candidates by node, with their keys' revisions and the newest write. // candidatesOf is a proposal's candidates by node, with their keys' revisions and the newest write.
@@ -194,8 +193,8 @@ func (p *passer) answersTo(id string) map[string]*nodeengine.CandidateAnswer {
return out return out
} }
// placementSeat takes every open proposal one step further. // assignmentsSeat takes every open proposal one step further.
func (p *passer) placementSeat(ctx context.Context) { func (p *passer) assignmentsSeat(ctx context.Context) {
for _, b := range seatBuckets { for _, b := range seatBuckets {
if p.kvs[b] == nil { if p.kvs[b] == nil {
return return
@@ -204,8 +203,10 @@ func (p *passer) placementSeat(ctx context.Context) {
if p.saidProposed == nil { if p.saidProposed == nil {
p.saidProposed, p.intended = map[string]bool{}, map[string]map[string]map[string]bool{} p.saidProposed, p.intended = map[string]bool{}, map[string]map[string]map[string]bool{}
} }
p.convertPlacement(ctx) if !p.copyRetired(ctx) {
current, revs := p.seatPlacements() return // the old state is copied whole before the seat takes a step: a half-copied state would place nothing on a node
}
current, revs := p.seatAssignments()
ids := make([]string, 0) ids := make([]string, 0)
raws := p.w.read(plc.ProposalsBucket) raws := p.w.read(plc.ProposalsBucket)
for id := range raws { for id := range raws {
@@ -251,7 +252,7 @@ func (p *passer) deletePulses(ctx context.Context, id string) {
// ask raises (or raises again) a proposal's authorising ask and its bound, and answers the proposal asked. // ask raises (or raises again) a proposal's authorising ask and its bound, and answers the proposal asked.
func (p *passer) ask(ctx context.Context, prop mma.Proposal, akey, brief string) (mma.Proposal, bool) { func (p *passer) ask(ctx context.Context, prop mma.Proposal, akey, brief string) (mma.Proposal, bool) {
a, err := assign.PlacementAsk(akey, prop, brief, assign.PlacementAsking(prop, p.clock()), p.clock()) a, err := assign.AssignmentsAsk(akey, prop, brief, assign.AssignmentsAsking(prop, p.clock()), p.clock())
if err != nil { if err != nil {
p.logf("the ask about the proposal %s is not written: %v", prop.ID, err) p.logf("the ask about the proposal %s is not written: %v", prop.ID, err)
return prop, false return prop, false
@@ -270,9 +271,9 @@ func (p *passer) ask(ctx context.Context, prop mma.Proposal, akey, brief string)
} }
// stepProposal takes one proposal one step further. // stepProposal takes one proposal one step further.
func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Placement, revs map[string]uint64) { func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Assignments, revs map[string]uint64) {
if !p.saidProposed[prop.ID] && prop.State == mmac.ProposalAnswering && prop.Verdict == nil { if !p.saidProposed[prop.ID] && prop.State == mmac.ProposalAnswering && prop.Verdict == nil {
p.sayEvent(mma.EventPlacementProposed, mma.PlacementEvent{Proposal: prop}, prop.ID) p.sayEvent(mma.EventAssignmentsProposed, mma.AssignmentsEvent{Proposal: prop}, prop.ID)
p.saidProposed[prop.ID] = true p.saidProposed[prop.ID] = true
} }
if prop.Withdrawn != "" { if prop.Withdrawn != "" {
@@ -281,14 +282,14 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
} }
if prop.Seed || prop.HandBack { if prop.Seed || prop.HandBack {
// Adopt and hand-back are gone since the lease write (ADR 0547 §11, ADR 0567 §11): a proposal one left open ends. // Adopt and hand-back are gone since the lease write (ADR 0547 §11, ADR 0567 §11): a proposal one left open ends.
p.endProposal(ctx, prop, "adopt and hand-back are gone since the lease write: placement is the mesh's alone") p.endProposal(ctx, prop, "adopt and hand-back are gone since the lease write: assignments is the mesh's alone")
return return
} }
if prop.Warrant != nil { if prop.Warrant != nil {
p.addNode(ctx, prop, revs) p.addNode(ctx, prop, revs)
return return
} }
akey, _ := asks.PlacementAskKey(prop.ID) akey, _ := asks.AssignmentsAskKey(prop.ID)
again := prop.Again != nil && !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At)) again := prop.Again != nil && !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At))
if prop.State == mmac.ProposalAsked { if prop.State == mmac.ProposalAsked {
askPulse, _ := plc.PulseAskKey(prop.ID) askPulse, _ := plc.PulseAskKey(prop.ID)
@@ -349,7 +350,7 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
} }
} }
if p.writeProposal(ctx, prop, rev) { if p.writeProposal(ctx, prop, rev) {
p.sayEvent(mma.EventPlacementJudged, mma.PlacementEvent{Proposal: prop}, p.sayEvent(mma.EventAssignmentsJudged, mma.AssignmentsEvent{Proposal: prop},
fmt.Sprintf("%s.judged.%d", prop.ID, v.At.UnixNano())) fmt.Sprintf("%s.judged.%d", prop.ID, v.At.UnixNano()))
} }
} }
@@ -371,7 +372,7 @@ func moved(prop mma.Proposal, revs map[string]uint64) bool {
} }
// intendedOf is, by proposal and the revisions it was laid over, what Intended computed, kept while this process runs. // intendedOf is, by proposal and the revisions it was laid over, what Intended computed, kept while this process runs.
func (p *passer) intendedOf(prop mma.Proposal, current map[string]mma.Placement, cands map[string]mma.CandidatesByProposalNodeValue) map[string]map[string]bool { func (p *passer) intendedOf(prop mma.Proposal, current map[string]mma.Assignments, cands map[string]mma.CandidatesByProposalNodeValue) map[string]map[string]bool {
key := fmt.Sprint(prop.ID, prop.Base) key := fmt.Sprint(prop.ID, prop.Base)
if got, ok := p.intended[key]; ok { if got, ok := p.intended[key]; ok {
return got return got
@@ -381,14 +382,14 @@ func (p *passer) intendedOf(prop mma.Proposal, current map[string]mma.Placement,
return got return got
} }
// compute is what the proposal changes on each node by its placement, and the nodes that answer it: every node with a // compute is what the proposal changes on each node by its assignments, and the nodes that answer it: every node with a
// placement or a record (judge.Intended, ADR 0593 §8). The holder composes nothing: each node says what its compositions // assignments or a record (judge.Intended, ADR 0593 §8). The holder composes nothing: each node says what its compositions
// change in its answer. // change in its answer.
func (p *passer) compute(prop mma.Proposal, current map[string]mma.Placement, cands map[string]mma.CandidatesByProposalNodeValue) (map[string]map[string]bool, []string) { func (p *passer) compute(prop mma.Proposal, current map[string]mma.Assignments, cands map[string]mma.CandidatesByProposalNodeValue) (map[string]map[string]bool, []string) {
laid := map[string]*mma.Placement{} laid := map[string]*mma.Assignments{}
for _, node := range plc.Nodes(prop) { for _, node := range plc.Nodes(prop) {
if c, ok := cands[node]; ok && !c.Reached { if c, ok := cands[node]; ok && !c.Reached {
laid[node] = c.Placement laid[node] = c.Assignments
} }
} }
nodes := map[string]bool{} nodes := map[string]bool{}
@@ -403,7 +404,7 @@ func (p *passer) compute(prop mma.Proposal, current map[string]mma.Placement, ca
// mustAnswer finds the nodes the proposal must hear from (§3), writes a reached candidate for each whose key it does not // mustAnswer finds the nodes the proposal must hear from (§3), writes a reached candidate for each whose key it does not
// change, and writes them into the proposal. // change, and writes them into the proposal.
func (p *passer) mustAnswer(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Placement, cands map[string]mma.CandidatesByProposalNodeValue) { func (p *passer) mustAnswer(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Assignments, cands map[string]mma.CandidatesByProposalNodeValue) {
for _, node := range plc.Nodes(prop) { for _, node := range plc.Nodes(prop) {
if _, ok := cands[node]; !ok { if _, ok := cands[node]; !ok {
return // the verbs write the candidates before the proposal: wait to hear them return // the verbs write the candidates before the proposal: wait to hear them
@@ -421,7 +422,7 @@ func (p *passer) mustAnswer(ctx context.Context, prop mma.Proposal, rev uint64,
} }
c := mma.CandidatesByProposalNodeValue{Proposal: prop.ID, Reached: true} c := mma.CandidatesByProposalNodeValue{Proposal: prop.ID, Reached: true}
if pl, ok := current[node]; ok { if pl, ok := current[node]; ok {
c.Placement = &pl c.Assignments = &pl
} }
p.put(ctx, plc.CandidatesBucket, key, func() ([]byte, error) { return plc.EncodeCandidate(c) }) p.put(ctx, plc.CandidatesBucket, key, func() ([]byte, error) { return plc.EncodeCandidate(c) })
} }
@@ -436,7 +437,7 @@ func (p *passer) mustAnswer(ctx context.Context, prop mma.Proposal, rev uint64,
// layAgain lays a proposal over the keys as they stand now (§5: a key moved): its candidates are written again, the // layAgain lays a proposal over the keys as they stand now (§5: a key moved): its candidates are written again, the
// nodes that must answer found again, and it is judged and asked again; a proposal that no longer lays ends. // nodes that must answer found again, and it is judged and asked again; a proposal that no longer lays ends.
func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Placement, revs map[string]uint64, why string) { func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Assignments, revs map[string]uint64, why string) {
held, recorded := p.lays(ctx) held, recorded := p.lays(ctx)
again, laid, findings := assign.Propose(assign.ProposeIn{ID: prop.ID, By: prop.By, Why: prop.Why, At: prop.At, Changes: prop.Changes, again, laid, findings := assign.Propose(assign.ProposeIn{ID: prop.ID, By: prop.By, Why: prop.Why, At: prop.At, Changes: prop.Changes,
Current: current, Revisions: revs, Held: held, Recorded: recorded}) Current: current, Revisions: revs, Held: held, Recorded: recorded})
@@ -448,7 +449,7 @@ func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, cu
p.deletePulses(ctx, prop.ID) p.deletePulses(ctx, prop.ID)
for _, node := range plc.Nodes(again) { for _, node := range plc.Nodes(again) {
key, _ := plc.CandidateKey(prop.ID, node) key, _ := plc.CandidateKey(prop.ID, node)
c := mma.CandidatesByProposalNodeValue{Proposal: prop.ID, Placement: laid[node]} c := mma.CandidatesByProposalNodeValue{Proposal: prop.ID, Assignments: laid[node]}
p.put(ctx, plc.CandidatesBucket, key, func() ([]byte, error) { return plc.EncodeCandidate(c) }) p.put(ctx, plc.CandidatesBucket, key, func() ([]byte, error) { return plc.EncodeCandidate(c) })
} }
again.Written, again.Words = ptr(p.clock().UTC()), why+": laid again over the keys as they stand, to be judged and asked again" again.Written, again.Words = ptr(p.clock().UTC()), why+": laid again over the keys as they stand, to be judged and asked again"
@@ -469,9 +470,9 @@ func (p *passer) lays(ctx context.Context) (func(string) bool, func(string) bool
} }
// warranted writes an approved proposal (§5): each changed key by compare-and-set at the revision it was laid over; then the warrant recorded, the candidates and the // warranted writes an approved proposal (§5): each changed key by compare-and-set at the revision it was laid over; then the warrant recorded, the candidates and the
// proposal deleted, and placement-changed said. // proposal deleted, and assignments-changed said.
func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, akey string, current map[string]mma.Placement, revs map[string]uint64) { func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, akey string, current map[string]mma.Assignments, revs map[string]uint64) {
if err := p.performs(akey, assign.PlacementAct(prop)); err != nil { if err := p.performs(akey, assign.AssignmentsAct(prop)); err != nil {
p.endProposal(ctx, prop, "its warrant does not bind these changes, so nothing is written: "+err.Error()) p.endProposal(ctx, prop, "its warrant does not bind these changes, so nothing is written: "+err.Error())
return return
} }
@@ -481,9 +482,9 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
return return
} }
cands, _, _ := p.candidatesOf(prop.ID) cands, _, _ := p.candidatesOf(prop.ID)
laid := map[string]*mma.Placement{} laid := map[string]*mma.Assignments{}
for _, node := range plc.Nodes(prop) { for _, node := range plc.Nodes(prop) {
laid[node] = cands[node].Placement laid[node] = cands[node].Assignments
} }
writes, err := assign.Writes(prop, laid) writes, err := assign.Writes(prop, laid)
if err != nil { if err != nil {
@@ -491,7 +492,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
return return
} }
var changes []plc.Change var changes []plc.Change
writtenLaid := map[string]*mma.Placement{} writtenLaid := map[string]*mma.Assignments{}
after := maps.Clone(current) after := maps.Clone(current)
afterRevs := maps.Clone(revs) afterRevs := maps.Clone(revs)
for _, w := range writes { for _, w := range writes {
@@ -499,7 +500,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
if err != nil { if err != nil {
// Another writer came first (a hand-back): what is written stands and is followed through; the rest is laid // Another writer came first (a hand-back): what is written stands and is followed through; the rest is laid
// again over the keys as this write left them. // again over the keys as this write left them.
p.logf("placement %s is not written at revision %d (%v): the rest of the proposal %s is laid again", w.Key, w.Base, err, prop.ID) p.logf("assignments %s is not written at revision %d (%v): the rest of the proposal %s is laid again", w.Key, w.Base, err, prop.ID)
p.followWrite(prop, answer, changes) p.followWrite(prop, answer, changes)
if rest, ok := assign.Rest(prop, writtenLaid); ok { if rest, ok := assign.Rest(prop, writtenLaid); ok {
p.layAgain(ctx, rest, rev, after, afterRevs, "a key moved while it was written, after "+strings.Join(sortedNodes(writtenLaid), ", ")+" were") p.layAgain(ctx, rest, rev, after, afterRevs, "a key moved while it was written, after "+strings.Join(sortedNodes(writtenLaid), ", ")+" were")
@@ -509,7 +510,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
return return
} }
before := current[w.Node] before := current[w.Node]
now := mma.Placement{Node: w.Node} now := mma.Assignments{Node: w.Node}
if w.Value != nil { if w.Value != nil {
now = *w.Value now = *w.Value
after[w.Node] = *w.Value after[w.Node] = *w.Value
@@ -532,9 +533,9 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
// addNode writes a joining node on the warrant of the enrolment that issued its token (ADR 0587 §7): the operator's Approve // addNode writes a joining node on the warrant of the enrolment that issued its token (ADR 0587 §7): the operator's Approve
// was the issue's, so no node is waited on and no ask is raised. The key is created at the revision it was laid over (none: a // was the issue's, so no node is waited on and no ask is raised. The key is created at the revision it was laid over (none: a
// node with a key is no joining node); the enrolment recorded the warrant in the hand-act log when it was given, so this does // node with a key is no joining node); the enrolment recorded the warrant in the hand-act log when it was given, so this does
// not record it again; placement-changed carries the warrant. A key written meanwhile ends the proposal, saying so. // not record it again; assignments-changed carries the warrant. A key written meanwhile ends the proposal, saying so.
func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string]uint64) { func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string]uint64) {
current, _ := p.seatPlacements() current, _ := p.seatAssignments()
if moved(prop, revs) { if moved(prop, revs) {
p.endProposal(ctx, prop, "a key it was laid over was written since: the enrolment proposes the node again") p.endProposal(ctx, prop, "a key it was laid over was written since: the enrolment proposes the node again")
return return
@@ -556,7 +557,7 @@ func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string
p.endProposal(ctx, prop, "the node's key was not written: "+err.Error()) p.endProposal(ctx, prop, "the node's key was not written: "+err.Error())
return return
} }
c := plc.ChangeOf(mma.Placement{Node: node}, value) c := plc.ChangeOf(mma.Assignments{Node: node}, value)
c.Before, c.After = uint64(prop.Base[node]), rev c.Before, c.After = uint64(prop.Base[node]), rev
p.deleteCandidates(ctx, prop.ID) p.deleteCandidates(ctx, prop.ID)
p.deletePulses(ctx, prop.ID) p.deletePulses(ctx, prop.ID)
@@ -569,9 +570,9 @@ func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string
} }
// removeNode deletes the key of a joining node whose enrolment ended unconfirmed, on that enrolment's warrant (the operator's // removeNode deletes the key of a joining node whose enrolment ended unconfirmed, on that enrolment's warrant (the operator's
// decision of 2026-11-02 01:05): no node answers and no ask is raised, as for its add-node; placement-changed carries the warrant. // decision of 2026-11-02 01:05): no node answers and no ask is raised, as for its add-node; assignments-changed carries the warrant.
// A node that runs modules or holds seats is no joining node and stays (LayChanges' finding ends the proposal). // A node that runs modules or holds seats is no joining node and stays (LayChanges' finding ends the proposal).
func (p *passer) removeNode(ctx context.Context, prop mma.Proposal, current map[string]mma.Placement, findings []string) { func (p *passer) removeNode(ctx context.Context, prop mma.Proposal, current map[string]mma.Assignments, findings []string) {
node := prop.Changes[0].Node node := prop.Changes[0].Node
before, ok := current[node] before, ok := current[node]
if len(findings) > 0 || !ok { if len(findings) > 0 || !ok {
@@ -583,7 +584,7 @@ func (p *passer) removeNode(ctx context.Context, prop mma.Proposal, current map[
p.endProposal(ctx, prop, "the node's key was not deleted: "+err.Error()) p.endProposal(ctx, prop, "the node's key was not deleted: "+err.Error())
return return
} }
c := plc.ChangeOf(before, mma.Placement{Node: node}) c := plc.ChangeOf(before, mma.Assignments{Node: node})
c.Before, c.After = uint64(prop.Base[node]), rev c.Before, c.After = uint64(prop.Base[node]), rev
p.deleteCandidates(ctx, prop.ID) p.deleteCandidates(ctx, prop.ID)
p.deletePulses(ctx, prop.ID) p.deletePulses(ctx, prop.ID)
@@ -591,12 +592,12 @@ func (p *passer) removeNode(ctx context.Context, prop mma.Proposal, current map[
p.w.put(plc.ProposalsBucket, prop.ID, nil) p.w.put(plc.ProposalsBucket, prop.ID, nil)
w := prop.Warrant w := prop.Warrant
p.sayChanged(prop, mdc.Answer{Option: assign.OptionYes, Warrant: w.Warrant, p.sayChanged(prop, mdc.Answer{Option: assign.OptionYes, Warrant: w.Warrant,
Words: fmt.Sprintf("the machine %s left placement: its enrolment (ask %s) ended before it confirmed", w.Node, w.Ask)}, Words: fmt.Sprintf("the machine %s left assignments: its enrolment (ask %s) ended before it confirmed", w.Node, w.Ask)},
[]plc.Change{c}) []plc.Change{c})
} }
// followWrite follows the keys a warrant wrote: the warrant recorded and placement-changed said. No verb of the // followWrite follows the keys a warrant wrote: the warrant recorded and assignments-changed said. No verb of the
// controller is called: the placement is the one source of where modules run (ADR 0567 §8, issue 774). // controller is called: the assignments is the one source of where modules run (ADR 0567 §8, issue 774).
func (p *passer) followWrite(prop mma.Proposal, answer mdc.Answer, changes []plc.Change) { func (p *passer) followWrite(prop mma.Proposal, answer mdc.Answer, changes []plc.Change) {
if len(changes) == 0 { if len(changes) == 0 {
return return
@@ -605,7 +606,7 @@ func (p *passer) followWrite(prop mma.Proposal, answer mdc.Answer, changes []plc
p.sayChanged(prop, answer, changes) p.sayChanged(prop, answer, changes)
} }
func sortedNodes(m map[string]*mma.Placement) []string { func sortedNodes(m map[string]*mma.Assignments) []string {
out := make([]string, 0, len(m)) out := make([]string, 0, len(m))
for n := range m { for n := range m {
out = append(out, n) out = append(out, n)
@@ -614,7 +615,7 @@ func sortedNodes(m map[string]*mma.Placement) []string {
return out return out
} }
// sayChanged says placement-changed for the keys written. // sayChanged says assignments-changed for the keys written.
func (p *passer) sayChanged(prop mma.Proposal, answer mdc.Answer, changes []plc.Change) { func (p *passer) sayChanged(prop mma.Proposal, answer mdc.Answer, changes []plc.Change) {
if len(changes) == 0 { if len(changes) == 0 {
return return
@@ -626,14 +627,14 @@ func (p *passer) sayChanged(prop mma.Proposal, answer mdc.Answer, changes []plc.
} }
} }
plc.SortChanges(changes) plc.SortChanges(changes)
p.sayEvent(mma.EventPlacementChanged, mma.PlacementEvent{Proposal: prop, Nodes: changeBodies(changes), p.sayEvent(mma.EventAssignmentsChanged, mma.AssignmentsEvent{Proposal: prop, Nodes: changeBodies(changes),
Warrant: &mma.PlacementEventWarrant{Warrant: answer.Warrant, Words: answer.Words, At: at}}, prop.ID+".changed") Warrant: &mma.AssignmentsEventWarrant{Warrant: answer.Warrant, Words: answer.Words, At: at}}, prop.ID+".changed")
} }
// endProposal ends a proposal: its ask, its candidates and its key deleted, and placement-withdrawn said carrying it // endProposal ends a proposal: its ask, its candidates and its key deleted, and assignments-withdrawn said carrying it
// whole with why it ended. // whole with why it ended.
func (p *passer) endProposal(ctx context.Context, prop mma.Proposal, why string) { func (p *passer) endProposal(ctx context.Context, prop mma.Proposal, why string) {
if akey, err := asks.PlacementAskKey(prop.ID); err == nil && p.w.has(buckets.Asks, akey) { if akey, err := asks.AssignmentsAskKey(prop.ID); err == nil && p.w.has(buckets.Asks, akey) {
if err := p.kvs[buckets.Asks].Delete(ctx, akey); err == nil { if err := p.kvs[buckets.Asks].Delete(ctx, akey); err == nil {
p.w.put(buckets.Asks, akey, nil) p.w.put(buckets.Asks, akey, nil)
} }
@@ -646,7 +647,7 @@ func (p *passer) endProposal(ctx context.Context, prop mma.Proposal, why string)
} }
p.w.put(plc.ProposalsBucket, prop.ID, nil) p.w.put(plc.ProposalsBucket, prop.ID, nil)
p.logf("the proposal %s ends: %s", prop.ID, why) p.logf("the proposal %s ends: %s", prop.ID, why)
p.sayEvent(mma.EventPlacementWithdrawn, mma.PlacementEvent{Proposal: prop, Ended: why}, prop.ID+".withdrawn") p.sayEvent(mma.EventAssignmentsWithdrawn, mma.AssignmentsEvent{Proposal: prop, Ended: why}, prop.ID+".withdrawn")
} }
// deleteCandidates deletes every candidate of a proposal. // deleteCandidates deletes every candidate of a proposal.
@@ -686,7 +687,7 @@ func (p *passer) writeProposal(ctx context.Context, prop mma.Proposal, rev uint6
} }
// say says one of the seat's events, said in the journal when the bus refuses it. // say says one of the seat's events, said in the journal when the bus refuses it.
func (p *passer) sayEvent(event string, body mma.PlacementEvent, id string) { func (p *passer) sayEvent(event string, body mma.AssignmentsEvent, id string) {
if p.publish == nil { if p.publish == nil {
return return
} }
@@ -695,30 +696,30 @@ func (p *passer) sayEvent(event string, body mma.PlacementEvent, id string) {
} }
} }
// writeNode writes one node's placement after (nil: none) as its keys (mesh-sdk mmac.Keys.Writes; research // writeNode writes one node's assignments after (nil: none) as its keys (mesh-sdk mmac.Keys.Writes; research
// 072 row 37): the module keys that change, each carrying the value it replaces, then the node key, the commit, then the // 072 row 37): the module keys that change, each carrying the value it replaces, then the node key, the commit, then the
// module keys no longer placed. It answers the node key's revision after (0: deleted). A write that fails up to the // module keys no longer placed. It answers the node key's revision after (0: deleted). A write that fails up to the
// commit ends it with nothing changed for any reader, the module keys written before it read at their previous values; // commit ends it with nothing changed for any reader, the module keys written before it read at their previous values;
// a delete after the commit that fails leaves a module key the node key does not name, which places nothing and is // a delete after the commit that fails leaves a module key the node key does not name, which places nothing and is
// deleted at the node's next write. Each key written is taken into the world at once, so the next write of the pass // deleted at the node's next write. Each key written is taken into the world at once, so the next write of the pass
// reads it. // reads it.
func (p *passer) writeNode(ctx context.Context, node string, after *mma.Placement) (uint64, error) { func (p *passer) writeNode(ctx context.Context, node string, after *mma.Assignments) (uint64, error) {
writes, err := p.w.placement.Writes(node, after) writes, err := p.w.assignmentKeys.Writes(node, after)
if err != nil { if err != nil {
return 0, err return 0, err
} }
kv := p.kvs[plc.PlacementBucket] kv := p.kvs[plc.AssignmentsBucket]
committed, rev := false, uint64(0) committed, rev := false, uint64(0)
for _, w := range writes { for _, w := range writes {
got, err := writeKey(ctx, kv, w) got, err := writeKey(ctx, kv, w)
if err != nil { if err != nil {
if committed { if committed {
p.logf("placement %s is not deleted after %s's write (%v): it places nothing, and goes at the node's next write", w.Key, node, err) p.logf("assignments %s is not deleted after %s's write (%v): it places nothing, and goes at the node's next write", w.Key, node, err)
continue continue
} }
return 0, fmt.Errorf("placement %s at revision %d: %w", w.Key, w.Base, err) return 0, fmt.Errorf("assignments %s at revision %d: %w", w.Key, w.Base, err)
} }
p.w.putEntry(plc.PlacementBucket, w.Key, w.Value, got, p.clock()) p.w.putEntry(plc.AssignmentsBucket, w.Key, w.Value, got, p.clock())
if w.Commit { if w.Commit {
committed, rev = true, got committed, rev = true, got
} }
@@ -726,7 +727,7 @@ func (p *passer) writeNode(ctx context.Context, node string, after *mma.Placemen
return rev, nil return rev, nil
} }
// writeKey makes one write of a placement key by compare-and-set and answers the key's revision after (0: deleted). // writeKey makes one write of an assignments key by compare-and-set and answers the key's revision after (0: deleted).
func writeKey(ctx context.Context, kv jetstream.KeyValue, w mmac.KeyWrite) (uint64, error) { func writeKey(ctx context.Context, kv jetstream.KeyValue, w mmac.KeyWrite) (uint64, error) {
switch { switch {
case w.Value == nil: case w.Value == nil:
@@ -738,33 +739,6 @@ func writeKey(ctx context.Context, kv jetstream.KeyValue, w mmac.KeyWrite) (uint
} }
} }
// convertPlacement converts every node the state holds only in the form before, `placement.<node>`, into its node key
// and module keys (research 072 row 37), once: each node converted is said, and a node whose conversion fails is said
// and tried again at the next pass. The legacy key is left for the readers not yet delivered; a node key wins over it
// for every reader that reads both, and it is deleted with the node key when the node's placement is.
func (p *passer) convertPlacement(ctx context.Context) {
if p.convertedAll {
return
}
migrations, err := p.w.placement.Migrations()
if err != nil {
p.logf("placement: some nodes in the form before cannot be converted, and stay read from it: %v", err)
}
failed := false
for _, node := range slices.Sorted(maps.Keys(migrations)) {
before, rev, _ := p.w.placement.Of(node)
got, err := p.writeNode(ctx, node, &before)
if err != nil {
failed = true
p.logf("placement: %s is not converted from placement.%s (revision %d) to its node and module keys, and is tried again at the next pass: %v", node, node, rev, err)
continue
}
p.logf("placement: %s converted from placement.%s (revision %d) to %s.node (revision %d) and %d module keys (research 072 row 37); placement.%s stays for readers not yet delivered",
node, node, rev, node, got, len(before.Modules), node)
}
p.convertedAll = !failed && err == nil
}
func ptr[T any](v T) *T { return &v } func ptr[T any](v T) *T { return &v }
// changeBodies are the changes as the seat's event carries them. // changeBodies are the changes as the seat's event carries them.
@@ -14,12 +14,12 @@ import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// The seat mesh-module-assigner's seven verbs (novox/hq ADR 0567 §10), served through the tool runner beside the put tools: a // The seat mesh-module-assigner's seven verbs (novox/hq ADR 0567 §10), served through the tool runner beside the put tools: a
@@ -59,18 +59,18 @@ func newSeatTools(st seatStore, ask func(string, any) (json.RawMessage, error))
return &seatTools{st: st, ask: ask, now: time.Now} return &seatTools{st: st, ask: ask, now: time.Now}
} }
// keyed is one placement key as a verb answers it. // keyed is one assignments key as a verb answers it.
type keyed struct { type keyed struct {
Placement mma.Placement `json:"placement"` Assignments mma.Assignments `json:"assignments"`
Revision uint64 `json:"revision"` Revision uint64 `json:"revision"`
} }
// placements reads every node's placement and the revision of its node key (its legacy key's before it is converted), // assignments reads every node's assignments and the revision of its node key (its legacy key's before it is converted),
// assembled from its keys in either form (mmac.Keys; research 072 row 37). // assembled from its keys in either form (mmac.Keys; research 072 row 37).
func (s *seatTools) placements() (map[string]mma.Placement, map[string]uint64, error) { func (s *seatTools) assignments() (map[string]mma.Assignments, map[string]uint64, error) {
keys, err := s.st.keys(mma.StatePlacement) keys, err := s.st.keys(mma.StateAssignments)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("placement cannot be listed: %w", err) return nil, nil, fmt.Errorf("assignments cannot be listed: %w", err)
} }
sort.Strings(keys) sort.Strings(keys)
held := mmac.NewKeys() held := mmac.NewKeys()
@@ -79,9 +79,9 @@ func (s *seatTools) placements() (map[string]mma.Placement, map[string]uint64, e
if _, _, _, err := mmac.ParseKey(key); err != nil { if _, _, _, err := mmac.ParseKey(key); err != nil {
continue continue
} }
raw, rev, found, err := s.st.get(mma.StatePlacement, key) raw, rev, found, err := s.st.get(mma.StateAssignments, key)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("placement key %s cannot be read: %w", key, err) return nil, nil, fmt.Errorf("assignments key %s cannot be read: %w", key, err)
} }
if !found { if !found {
continue continue
@@ -92,7 +92,7 @@ func (s *seatTools) placements() (map[string]mma.Placement, map[string]uint64, e
} }
out, revs := held.AllRevisions() out, revs := held.AllRevisions()
if len(bad) > 0 && len(out) == 0 { if len(bad) > 0 && len(out) == 0 {
return nil, nil, fmt.Errorf("placement cannot be read: %s", strings.Join(bad, "; ")) return nil, nil, fmt.Errorf("assignments cannot be read: %s", strings.Join(bad, "; "))
} }
return out, revs, nil return out, revs, nil
} }
@@ -145,14 +145,14 @@ func (s *seatTools) proposal(id string) (mma.Proposal, error) {
// write writes a proposal laid by assign.Propose: each candidate, then the proposal, whose write the assigner's process // write writes a proposal laid by assign.Propose: each candidate, then the proposal, whose write the assigner's process
// takes as the event to compose and judge it. // takes as the event to compose and judge it.
func (s *seatTools) write(p mma.Proposal, laid map[string]*mma.Placement) error { func (s *seatTools) write(p mma.Proposal, laid map[string]*mma.Assignments) error {
p.Written = ptr(s.now().UTC()) p.Written = ptr(s.now().UTC())
for _, node := range plc.Nodes(p) { for _, node := range plc.Nodes(p) {
key, err := plc.CandidateKey(p.ID, node) key, err := plc.CandidateKey(p.ID, node)
if err != nil { if err != nil {
return err return err
} }
raw, err := plc.EncodeCandidate(mma.CandidatesByProposalNodeValue{Proposal: p.ID, Placement: laid[node]}) raw, err := plc.EncodeCandidate(mma.CandidatesByProposalNodeValue{Proposal: p.ID, Assignments: laid[node]})
if err != nil { if err != nil {
return err return err
} }
@@ -215,7 +215,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
return nil, errors.New("the seed is gone with adopt: propose takes place, take-off, pin, unpin, hold, unhold, decide and config") return nil, errors.New("the seed is gone with adopt: propose takes place, take-off, pin, unpin, hold, unhold, decide and config")
} }
} }
current, revs, err := s.placements() current, revs, err := s.assignments()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -282,7 +282,7 @@ func moduleOfCaller(by string, enrollers []string) string {
} }
// doWithdraw is the verb withdraw (§2): the proposal is marked, and the assigner deletes it and its candidates and says // doWithdraw is the verb withdraw (§2): the proposal is marked, and the assigner deletes it and its candidates and says
// placement-withdrawn carrying it whole. // assignments-withdrawn carrying it whole.
func (s *seatTools) doWithdraw(args map[string]any, by string) (any, error) { func (s *seatTools) doWithdraw(args map[string]any, by string) (any, error) {
why := arg(args, "why") why := arg(args, "why")
if why == "" { if why == "" {
@@ -321,8 +321,8 @@ func (s *seatTools) doAgain(args map[string]any) (any, error) {
return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil
} }
// placementAnswer is what the verb placement answers. // assignmentsAnswer is what the verb assignments answers.
type placementAnswer struct { type assignmentsAnswer struct {
Keys map[string]keyed `json:"keys,omitempty"` Keys map[string]keyed `json:"keys,omitempty"`
Nodes []string `json:"nodes,omitempty"` Nodes []string `json:"nodes,omitempty"`
Count int `json:"count"` Count int `json:"count"`
@@ -340,18 +340,18 @@ type historyEntry struct {
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
} }
// doPlacement is the verb placement (§10, §6): one node's key, one module's nodes, or every key; with history, the // doAssignments is the verb assignments (§10, §6): one node's key, one module's nodes, or every key; with history, the
// changes in order from the revisions the state keeps. // changes in order from the revisions the state keeps.
func (s *seatTools) doPlacement(args map[string]any) (any, error) { func (s *seatTools) doAssignments(args map[string]any) (any, error) {
if arg(args, "seats") == "true" { if arg(args, "seats") == "true" {
return s.seats(arg(args, "node")) return s.seats(arg(args, "node"))
} }
all, revs, err := s.placements() all, revs, err := s.assignments()
if err != nil { if err != nil {
return nil, err return nil, err
} }
node, module := arg(args, "node"), arg(args, "module") node, module := arg(args, "node"), arg(args, "module")
out := placementAnswer{Keys: map[string]keyed{}, Count: len(all), History: plc.PlacementHistory} out := assignmentsAnswer{Keys: map[string]keyed{}, Count: len(all), History: plc.AssignmentsHistory}
for n, p := range all { for n, p := range all {
if node != "" && n != node { if node != "" && n != node {
continue continue
@@ -359,12 +359,12 @@ func (s *seatTools) doPlacement(args map[string]any) (any, error) {
if module != "" && !strings.Contains(" "+strings.Join(mmac.Assigned(p), " ")+" ", " "+module+" ") { if module != "" && !strings.Contains(" "+strings.Join(mmac.Assigned(p), " ")+" ", " "+module+" ") {
continue continue
} }
out.Keys[n] = keyed{Placement: p, Revision: revs[n]} out.Keys[n] = keyed{Assignments: p, Revision: revs[n]}
out.Nodes = append(out.Nodes, n) out.Nodes = append(out.Nodes, n)
} }
sort.Strings(out.Nodes) sort.Strings(out.Nodes)
if node != "" && len(out.Keys) == 0 && arg(args, "history") != "true" { if node != "" && len(out.Keys) == 0 && arg(args, "history") != "true" {
return nil, fmt.Errorf("%s has no placement key", node) return nil, fmt.Errorf("%s has no assignments key", node)
} }
if arg(args, "history") != "true" { if arg(args, "history") != "true" {
return out, nil return out, nil
@@ -402,8 +402,8 @@ func (s *seatTools) doPlacement(args map[string]any) (any, error) {
out.Changes = append(out.Changes, e) out.Changes = append(out.Changes, e)
} }
} }
out.Older = fmt.Sprintf("the state keeps %d revisions of each key; an older change is in its placement-changed event on "+ out.Older = fmt.Sprintf("the state keeps %d revisions of each key; an older change is in its assignments-changed event on "+
"the seat's subject on the shared events stream, kept %d days", plc.PlacementHistory, plc.EventDays) "the seat's subject on the shared events stream, kept %d days", plc.AssignmentsHistory, plc.EventDays)
return out, nil return out, nil
} }
@@ -483,11 +483,11 @@ func (s *seatTools) doProposals(args map[string]any) (any, error) {
// only from a tool of that name), with the seat's own schemas. // only from a tool of that name), with the seat's own schemas.
func (s *seatTools) tools() []stdio.Tool { func (s *seatTools) tools() []stdio.Tool {
run := map[string]func(map[string]any) (any, error){ run := map[string]func(map[string]any) (any, error){
mma.VerbPlacement: s.doPlacement, mma.VerbAssignments: s.doAssignments,
mma.VerbProposals: s.doProposals, mma.VerbProposals: s.doProposals,
mma.VerbPropose: func(a map[string]any) (any, error) { return s.doPropose(a, stdio.CallerOf(a)) }, mma.VerbPropose: func(a map[string]any) (any, error) { return s.doPropose(a, stdio.CallerOf(a)) },
mma.VerbWithdraw: func(a map[string]any) (any, error) { return s.doWithdraw(a, stdio.CallerOf(a)) }, mma.VerbWithdraw: func(a map[string]any) (any, error) { return s.doWithdraw(a, stdio.CallerOf(a)) },
mma.VerbAgain: s.doAgain, mma.VerbAgain: s.doAgain,
} }
var out []stdio.Tool var out []stdio.Tool
for _, v := range mma.Verbs() { for _, v := range mma.Verbs() {
@@ -496,7 +496,7 @@ func (s *seatTools) tools() []stdio.Tool {
return out return out
} }
// seats is the verb placement with seats (ADR 0571 §4): who holds each seat and on which node, as the state seat-holders // seats is the verb assignments with seats (ADR 0571 §4): who holds each seat and on which node, as the state seat-holders
// holds it; with node, the keys of seats held on that node. // holds it; with node, the keys of seats held on that node.
func (s *seatTools) seats(node string) (any, error) { func (s *seatTools) seats(node string) (any, error) {
keys, err := s.st.keys(mma.StateSeatHolders) keys, err := s.st.keys(mma.StateSeatHolders)
@@ -525,71 +525,51 @@ func (s *seatTools) seats(node string) (any, error) {
return map[string]any{"seats": out, "holds": len(keys)}, nil return map[string]any{"seats": out, "holds": len(keys)}, nil
} }
// keyedRevision is one revision of a node's placement key, the key it is of beside it. // keyedRevision is one revision of a node's assignments key, the key it is of beside it.
type keyedRevision struct { type keyedRevision struct {
stdio.StateRevision stdio.StateRevision
Key string Key string
} }
// nodeHistory is the revisions of a node's placement, in the order the bus wrote them: its legacy key's // nodeHistory is the revisions of a node's assignments, in the order the bus wrote them: its node key's `<node>.node`,
// `placement.<node>` before it was converted, then its node key's `<node>.node`, whose every revision is one write of // whose every revision is one write of the node (research 072 row 37: the node key is a write's commit, so its history is
// the node (research 072 row 37: the node key is a write's commit, so its history is the node's placement history). // the node's assignments history).
func (s *seatTools) nodeHistory(node string) ([]keyedRevision, error) { func (s *seatTools) nodeHistory(node string) ([]keyedRevision, error) {
legacy, err := mmac.LegacyKey(node)
if err != nil {
return nil, err
}
nodeKey, err := mmac.NodeKey(node) nodeKey, err := mmac.NodeKey(node)
if err != nil { if err != nil {
return nil, err return nil, err
} }
revisions, err := s.st.history(mma.StateAssignments, nodeKey)
if err != nil {
return nil, fmt.Errorf("the history of %s cannot be read: %w", nodeKey, err)
}
var out []keyedRevision var out []keyedRevision
for _, key := range []string{legacy, nodeKey} { for _, r := range revisions {
revisions, err := s.st.history(mma.StatePlacement, key) out = append(out, keyedRevision{StateRevision: r, Key: nodeKey})
if err != nil {
return nil, fmt.Errorf("the history of %s cannot be read: %w", key, err)
}
for _, r := range revisions {
out = append(out, keyedRevision{StateRevision: r, Key: key})
}
} }
sort.SliceStable(out, func(i, j int) bool { return out[i].Revision < out[j].Revision }) sort.SliceStable(out, func(i, j int) bool { return out[i].Revision < out[j].Revision })
return out, nil return out, nil
} }
// sketch is a revision of a node's placement as far as its history says a change: each module's digest, and its pins // sketch is a revision of a node's assignments as far as its history says a change: each module's digest, and its pins
// and holdings, which change every module (plc.ChangeOf). // and holdings, which change every module (plc.ChangeOf).
type sketch struct { type sketch struct {
modules []mma.PlacementByNodeNodeValueModulesItem modules []mma.AssignmentsByNodeNodeValueModulesItem
nodeWide string nodeWide string
} }
// sketchOf reads one revision of a node key or a legacy key; a value that does not read places nothing. // sketchOf reads one revision of a node key; a value that does not read places nothing.
func sketchOf(key string, value []byte) sketch { func sketchOf(key string, value []byte) sketch {
_, _, kind, err := mmac.ParseKey(key) _, _, kind, err := mmac.ParseKey(key)
if err != nil || kind != mmac.NodeKind {
return sketch{}
}
n, err := mmac.DecodeNode(value)
if err != nil { if err != nil {
return sketch{} return sketch{}
} }
wide := func(pins map[string]mma.Provider, holds []mma.Holding) string { raw, _ := json.Marshal([]any{n.Pins, n.Holds})
raw, _ := json.Marshal([]any{pins, holds}) return sketch{modules: n.Modules, nodeWide: string(raw)}
return string(raw)
}
if kind == mmac.NodeKind {
n, err := mmac.DecodeNode(value)
if err != nil {
return sketch{}
}
return sketch{modules: n.Modules, nodeWide: wide(n.Pins, n.Holds)}
}
p, err := mmac.DecodePlacement(value)
if err != nil {
return sketch{}
}
out := sketch{nodeWide: wide(p.Pins, p.Holds)}
for _, m := range p.Modules {
out.modules = append(out.modules, mma.PlacementByNodeNodeValueModulesItem{Module: m.Module, Digest: mmac.Digest(m)})
}
return out
} }
// changeFrom is what changed from before to s on node, as plc.ChangeOf says it. // changeFrom is what changed from before to s on node, as plc.ChangeOf says it.
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"os" "os"
"reflect" "reflect"
@@ -19,16 +20,16 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
func (s memStore) del(state, key string) error { func (s memStore) del(state, key string) error {
@@ -45,13 +46,13 @@ func (s memStore) history(state, key string) ([]stdio.StateRevision, error) {
} }
// ADR 0567 §2, §7, §10: the seat's five verbs over the states. propose lays a proposal and writes it with its candidates, // ADR 0567 §2, §7, §10: the seat's five verbs over the states. propose lays a proposal and writes it with its candidates,
// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; placement and // or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; assignments and
// proposals read every state the seat keeps, and history names the stream for what the state no longer keeps. // proposals read every state the seat keeps, and history names the stream for what the state no longer keeps.
func TestTheSeatsVerbs(t *testing.T) { func TestTheSeatsVerbs(t *testing.T) {
anchor := mma.Placement{V: keys.Version, Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}, {Module: "mesh-enrolment"}}, anchor := mma.Assignments{V: keys.Version, Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}, {Module: "mesh-enrolment"}},
Holds: []mma.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: "mesh-enrolment"}}} Holds: []mma.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: "mesh-enrolment"}}}
st := memStore{} st := memStore{}
for _, state := range []string{mma.StatePlacement, mma.StateProposals, mma.StateCandidates, modulesState, haveState} { for _, state := range []string{mma.StateAssignments, mma.StateProposals, mma.StateCandidates, modulesState, haveState} {
st[state] = &memKV{values: map[string][]byte{}} st[state] = &memKV{values: map[string][]byte{}}
} }
for _, m := range []string{"web", "shell", "mesh-enrolment"} { for _, m := range []string{"web", "shell", "mesh-enrolment"} {
@@ -68,8 +69,7 @@ func TestTheSeatsVerbs(t *testing.T) {
}) })
s.now = func() time.Time { return time.Date(2026, 10, 28, 20, 0, 0, 0, time.UTC) } s.now = func() time.Time { return time.Date(2026, 10, 28, 20, 0, 0, 0, time.UTC) }
raw, _ := mmac.EncodePlacement(anchor) setAssignments(st[mma.StateAssignments], anchor)
st[mma.StatePlacement].values["placement.anchor"] = raw
// withdraw and again mark the proposal; the holder ends or judges it. // withdraw and again mark the proposal; the holder ends or judges it.
got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op") got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op")
@@ -106,19 +106,19 @@ func TestTheSeatsVerbs(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
move := got.(proposed).Proposal move := got.(proposed).Proposal
if move.By != "operator@laptop" || move.Base["anchor"] != int64(len(raw)) || move.Base["laptop"] != 0 || len(st[mma.StateCandidates].values) != 2 { if move.By != "operator@laptop" || move.Base["anchor"] != int64(len(st[mma.StateAssignments].values["anchor.node"])) || move.Base["laptop"] != 0 || len(st[mma.StateCandidates].values) != 2 {
t.Fatalf("propose = %+v", move) t.Fatalf("propose = %+v", move)
} }
// placement and proposals read every state the seat keeps (§10); history names the stream for what is older. // assignments and proposals read every state the seat keeps (§10); history names the stream for what is older.
reads := map[string]bool{} reads := map[string]bool{}
pa, err := s.doPlacement(map[string]any{"module": "web", "history": "true"}) pa, err := s.doAssignments(map[string]any{"module": "web", "history": "true"})
if a := pa.(placementAnswer); err != nil || !slices.Equal(a.Nodes, []string{"anchor"}) || len(a.Changes) != 1 || !strings.Contains(a.Older, "shared events stream") { if a := pa.(assignmentsAnswer); err != nil || !slices.Equal(a.Nodes, []string{"anchor"}) || len(a.Changes) != 1 || !strings.Contains(a.Older, "shared events stream") {
t.Errorf("placement of web = %+v, %v", pa, err) t.Errorf("assignments of web = %+v, %v", pa, err)
} else { } else {
reads[mma.StatePlacement], reads["events"] = true, true reads[mma.StateAssignments], reads["events"] = true, true
} }
if _, err := s.doPlacement(map[string]any{"node": "laptop"}); err == nil { if _, err := s.doAssignments(map[string]any{"node": "laptop"}); err == nil {
t.Error("a node with no key was answered") t.Error("a node with no key was answered")
} }
ps, err := s.doProposals(map[string]any{"id": move.ID}) ps, err := s.doProposals(map[string]any{"id": move.ID})
@@ -132,7 +132,7 @@ func TestTheSeatsVerbs(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, state := range raw0.State { for _, state := range raw0.State {
if slices.Contains([]string{mma.StatePlacement, mma.StateProposals, mma.StateCandidates}, state.Name) && !reads[state.Name] { if slices.Contains([]string{mma.StateAssignments, mma.StateProposals, mma.StateCandidates}, state.Name) && !reads[state.Name] {
t.Errorf("no verb reads the seat's state %s", state.Name) t.Errorf("no verb reads the seat's state %s", state.Name)
} }
} }
@@ -142,7 +142,7 @@ func TestTheSeatsVerbs(t *testing.T) {
} }
} }
// Issue 774 (ADR 0567 §8): every kind of change is proposed, and there is no controller to hand placement back to. // Issue 774 (ADR 0567 §8): every kind of change is proposed, and there is no controller to hand assignments back to.
if _, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op"); err != nil { if _, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op"); err != nil {
t.Errorf("a hold: %v", err) t.Errorf("a hold: %v", err)
} }
@@ -168,11 +168,10 @@ func TestTheSeatsVerbs(t *testing.T) {
if err := addNode("novox.mesh-enrolment", good); err != nil { if err := addNode("novox.mesh-enrolment", good); err != nil {
t.Errorf("an add-node from mesh-enrolment: %v", err) t.Errorf("an add-node from mesh-enrolment: %v", err)
} }
// A remove-node (the operator's decision of 2026-11-02 01:05) takes a joining node that never confirmed off placement: from // A remove-node (the operator's decision of 2026-11-02 01:05) takes a joining node that never confirmed off assignments: from
// mesh-enrolment, on the warrant naming that node, of a node running nothing, it is proposed with the node's key to delete; // mesh-enrolment, on the warrant naming that node, of a node running nothing, it is proposed with the node's key to delete;
// of a node running modules, or with another node's warrant, it is refused. // of a node running modules, or with another node's warrant, it is refused.
joiner, _ := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: "joiner", Decided: mma.NodeDecided{Address: "10.9.0.7"}, Modules: []mma.PlacedModule{}}) setAssignments(st[mma.StateAssignments], mma.Assignments{V: keys.Version, Node: "joiner", Decided: mma.NodeDecided{Address: "10.9.0.7"}, Modules: []mma.PlacedModule{}})
st[mma.StatePlacement].values["placement.joiner"] = joiner
removeNode := func(node, warrant string) (any, error) { removeNode := func(node, warrant string) (any, error) {
return s.doPropose(map[string]any{"why": "enrolment ended", "warrant": warrant, return s.doPropose(map[string]any{"why": "enrolment ended", "warrant": warrant,
"changes": []any{map[string]any{"op": "remove-node", "node": node}}}, "novox.mesh-enrolment") "changes": []any{map[string]any{"op": "remove-node", "node": node}}}, "novox.mesh-enrolment")
@@ -198,9 +197,7 @@ func TestTheSeatsVerbs(t *testing.T) {
t.Run("1955/L7 (ADR 0571 §6): rule 7 reads the node's own composition; an unplaced node is said", func(t *testing.T) { t.Run("1955/L7 (ADR 0571 §6): rule 7 reads the node's own composition; an unplaced node is said", func(t *testing.T) {
w := newWorld() w := newWorld()
fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("2", 64) fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("2", 64)
pl, _ := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: "ace", Modules: []mma.PlacedModule{{Module: "web"}}}) putAssignments(w, mma.Assignments{V: keys.Version, Node: "ace", Modules: []mma.PlacedModule{{Module: "web"}}})
pkey, _ := mmac.LegacyKey("ace")
w.putEntry(plc.PlacementBucket, pkey, pl, 1, time.Now())
for node, digest := range map[string]string{"ace": "sha256:" + strings.Repeat("a", 64), "g14": ""} { for node, digest := range map[string]string{"ace": "sha256:" + strings.Repeat("a", 64), "g14": ""} {
hn, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Composed: digest}) hn, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Composed: digest})
hk, _ := nodeengineclient.HaveNodeKey(node) hk, _ := nodeengineclient.HaveNodeKey(node)
@@ -220,25 +217,18 @@ func TestTheSeatsVerbs(t *testing.T) {
if _, ok := findings[assign.UnplacedKey("g14")]; !ok || len(findings) != 1 { if _, ok := findings[assign.UnplacedKey("g14")]; !ok || len(findings) != 1 {
t.Fatalf("findings = %v", findings) t.Fatalf("findings = %v", findings)
} }
pl2, _ := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}}) putAssignments(w, mma.Assignments{V: keys.Version, Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
pkey2, _ := mmac.LegacyKey("g14")
w.putEntry(plc.PlacementBucket, pkey2, pl2, 1, time.Now())
p.unplacedFindings() p.unplacedFindings()
if len(findings) != 0 { if len(findings) != 0 {
t.Errorf("placed again: findings %v", findings) t.Errorf("placed again: findings %v", findings)
} }
}) })
t.Run("1955/L7 (ADR 0571 §4): seat-holders is written from placement, only the keys a change moves, and answered by placement seats", func(t *testing.T) { t.Run("1955/L7 (ADR 0571 §4): seat-holders is written from assignments, only the keys a change moves, and answered by assignments seats", func(t *testing.T) {
writes := 0 writes := 0
kv := &countingKV{memKV: memKV{values: map[string][]byte{}}, writes: &writes} kv := &countingKV{memKV: memKV{values: map[string][]byte{}}, writes: &writes}
p := &passer{w: newWorld(), logf: t.Logf, kvs: kvSet{plc.SeatHoldersBucket: kv}} p := &passer{w: newWorld(), logf: t.Logf, kvs: kvSet{plc.SeatHoldersBucket: kv}}
place := func(node string, holds ...mma.Holding) { place := func(node string, holds ...mma.Holding) {
pl, err := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: node, Modules: []mma.PlacedModule{{Module: "web"}, {Module: "nats"}}, Holds: holds}) putAssignments(p.w, mma.Assignments{V: keys.Version, Node: node, Modules: []mma.PlacedModule{{Module: "web"}, {Module: "nats"}}, Holds: holds})
if err != nil {
t.Fatal(err)
}
k, _ := mmac.LegacyKey(node)
p.w.putEntry(plc.PlacementBucket, k, pl, 1, time.Now())
} }
place("ace", mma.Holding{Claim: "mesh-broker", Scope: "mesh", Module: "nats"}, mma.Holding{Claim: "node-shell", Scope: "node", Module: "web"}) place("ace", mma.Holding{Claim: "mesh-broker", Scope: "mesh", Module: "nats"}, mma.Holding{Claim: "node-shell", Scope: "node", Module: "web"})
place("g14", mma.Holding{Claim: "node-shell", Scope: "node", Module: "web"}) place("g14", mma.Holding{Claim: "node-shell", Scope: "node", Module: "web"})
@@ -249,7 +239,7 @@ func TestTheSeatsVerbs(t *testing.T) {
} }
p.seatHolders(ctx) p.seatHolders(ctx)
if writes != 3 { if writes != 3 {
t.Errorf("an unchanged placement wrote %d keys", writes-3) t.Errorf("an unchanged assignments wrote %d keys", writes-3)
} }
place("g14") // g14 holds nothing now place("g14") // g14 holds nothing now
p.seatHolders(ctx) p.seatHolders(ctx)
@@ -262,68 +252,122 @@ func TestTheSeatsVerbs(t *testing.T) {
t.Errorf("seats on ace = %+v, %v", out, err) t.Errorf("seats on ace = %+v, %v", out, err)
} }
}) })
t.Run("research 072 row 37: the form before is converted once, a node is written as its keys, and history reads both", func(t *testing.T) { t.Run("research 072 row 37: a node is written as its keys, and history reads the node key", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
kv := &revKV{values: map[string][]byte{}, revs: map[string]uint64{}} kv := &revKV{values: map[string][]byte{}, revs: map[string]uint64{}}
var logs []string p := &passer{w: newWorld(), kvs: kvSet{plc.AssignmentsBucket: kv}, now: time.Now, logf: t.Logf}
p := &passer{w: newWorld(), kvs: kvSet{plc.PlacementBucket: kv}, now: time.Now, first := mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}
logf: func(f string, a ...any) { logs = append(logs, fmt.Sprintf(f, a...)) }} if _, err := p.writeNode(ctx, "anchor", &first); err != nil || strings.Join(kv.log, " ") != "create anchor.web create anchor.node" {
put := func(key string, value []byte, rev uint64) { t.Fatalf("the first write = %v, %v", kv.log, err)
p.w.putEntry(plc.PlacementBucket, key, value, rev, time.Now())
} }
was := mma.Placement{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}} raw := kv.values["anchor.node"]
raw, _ := mmac.EncodePlacement(was) // A write: the module keys, the node key (its commit), then the module key no longer placed.
put("placement.anchor", raw, kv.write("placement.anchor", raw)) after := mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell", Ports: map[string]int64{"22": 2222}}}}
p.convertPlacement(ctx)
if strings.Join(kv.log, " ") != "create anchor.web create anchor.node" || !p.convertedAll ||
!strings.Contains(strings.Join(logs, "\n"), "anchor converted from placement.anchor") {
t.Fatalf("the conversion wrote %v, said %v", kv.log, logs)
}
p.convertPlacement(ctx)
if len(kv.log) != 2 {
t.Errorf("a converted node is converted again: %v", kv.log)
}
// A write: the module keys, the node key (its commit), the mirror, then the module key no longer placed.
after := mma.Placement{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell", Ports: map[string]int64{"22": 2222}}}}
kv.log = nil kv.log = nil
rev, err := p.writeNode(ctx, "anchor", &after) rev, err := p.writeNode(ctx, "anchor", &after)
if err != nil || strings.Join(kv.log, " ") != "create anchor.shell update anchor.node update placement.anchor delete anchor.web" || rev != kv.revs["anchor.node"] { if err != nil || strings.Join(kv.log, " ") != "create anchor.shell update anchor.node delete anchor.web" || rev != kv.revs["anchor.node"] {
t.Fatalf("the write = %v, %d, %v", kv.log, rev, err) t.Fatalf("the write = %v, %d, %v", kv.log, rev, err)
} }
if got, _ := p.seatPlacements(); !mmac.SamePlacement(got["anchor"], after) { if got, _ := p.seatAssignments(); !mmac.SameAssignments(got["anchor"], after) {
t.Errorf("the write is not read back at once: %+v", got) t.Errorf("the write is not read back at once: %+v", got)
} }
// A commit refused leaves every reader on what stood: the module key written before it carries the value it replaced. // A commit refused leaves every reader on what stood: the module key written before it carries the value it replaced.
kv.refuse, kv.log = "anchor.node", nil kv.refuse, kv.log = "anchor.node", nil
moved := mma.Placement{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell", Ports: map[string]int64{"22": 2223}}}} moved := mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell", Ports: map[string]int64{"22": 2223}}}}
if _, err := p.writeNode(ctx, "anchor", &moved); err == nil || strings.Join(kv.log, " ") != "update anchor.shell" { if _, err := p.writeNode(ctx, "anchor", &moved); err == nil || strings.Join(kv.log, " ") != "update anchor.shell" {
t.Fatalf("a refused commit = %v, %v", kv.log, err) t.Fatalf("a refused commit = %v, %v", kv.log, err)
} }
fresh, err := mmac.PlacementsOf(kv.values) fresh, err := mmac.AssignmentsOf(kv.values)
if err != nil || !mmac.SamePlacement(fresh["anchor"], after) { if err != nil || !mmac.SameAssignments(fresh["anchor"], after) {
t.Errorf("a reader of the keys after a refused commit reads %+v, %v", fresh["anchor"], err) t.Errorf("a reader of the keys after a refused commit reads %+v, %v", fresh["anchor"], err)
} }
kv.refuse = "" kv.refuse = ""
// The verb's history reads the form before, then the node key. // The verb's history reads the node key.
st := memStore{mma.StatePlacement: &memKV{values: kv.values}} st := memStore{mma.StateAssignments: &memKV{values: kv.values}}
hs := newSeatTools(st, nil) hs := newSeatTools(st, nil)
hs.st = historyStore{memStore: st, revisions: map[string][]stdio.StateRevision{ hs.st = historyStore{memStore: st, revisions: map[string][]stdio.StateRevision{
"placement.anchor": {{Key: "placement.anchor", Value: raw, Revision: 1, Op: "put"}}, "anchor.node": {{Key: "anchor.node", Value: raw, Revision: 1, Op: "put"}, {Key: "anchor.node", Value: kv.values["anchor.node"], Revision: 5, Op: "put"}, {Key: "anchor.node", Revision: 9, Op: "del"}},
"anchor.node": {{Key: "anchor.node", Value: kv.values["anchor.node"], Revision: 5, Op: "put"}, {Key: "anchor.node", Revision: 9, Op: "del"}},
}} }}
pa, err := hs.doPlacement(map[string]any{"node": "anchor", "history": "true"}) pa, err := hs.doAssignments(map[string]any{"node": "anchor", "history": "true"})
a, _ := pa.(placementAnswer) a, _ := pa.(assignmentsAnswer)
if err != nil || len(a.Changes) != 3 || !slices.Equal(a.Changes[0].Change.Added, []string{"web"}) || if err != nil || len(a.Changes) != 3 || !slices.Equal(a.Changes[0].Change.Added, []string{"web"}) ||
!slices.Equal(a.Changes[1].Change.Added, []string{"shell"}) || !slices.Equal(a.Changes[1].Change.Removed, []string{"web"}) || !slices.Equal(a.Changes[1].Change.Added, []string{"shell"}) || !slices.Equal(a.Changes[1].Change.Removed, []string{"web"}) ||
!a.Changes[2].Deleted || !slices.Equal(a.Changes[2].Change.Removed, []string{"shell"}) { !a.Changes[2].Deleted || !slices.Equal(a.Changes[2].Change.Removed, []string{"shell"}) {
t.Errorf("history = %+v, %v", pa, err) t.Errorf("history = %+v, %v", pa, err)
} }
// No placement: the mirror first, then the node key, then the module keys. // No assignments: the node key, then the module keys.
kv.log = nil kv.log = nil
if _, err := p.writeNode(ctx, "anchor", nil); err != nil || strings.Join(kv.log, " ") != "delete placement.anchor delete anchor.node delete anchor.shell" { if _, err := p.writeNode(ctx, "anchor", nil); err != nil || strings.Join(kv.log, " ") != "delete anchor.node delete anchor.shell" {
t.Errorf("the delete = %v, %v", kv.log, err) t.Errorf("the delete = %v, %v", kv.log, err)
} }
}) })
t.Run("ADR 0641: the retired state is copied into assignments once, key by key, with no gap", func(t *testing.T) {
ctx := context.Background()
web, _ := mmac.AssignmentsValues(mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}})
shell, _ := mmac.AssignmentsValues(mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell"}}})
for _, c := range []struct {
name string
old, present map[string][]byte // the retired state, and what assignments holds already
wantLog string // the writes into assignments, in order
wantValues map[string][]byte // assignments afterwards
wantLeft []string // what the retired state still holds
said copiedSaid // the event; none when zero
}{
{"an empty new state takes every key, the node key last, and the old form is dropped",
map[string][]byte{"anchor.node": web["anchor.node"], "anchor.web": web["anchor.web"], "placement.anchor": []byte(`{"whole":1}`)},
nil, "create anchor.web create anchor.node", web, nil, copiedSaid{Copied: 2, Dropped: 1}},
{"a key already in the new state is kept as it is",
map[string][]byte{"anchor.node": web["anchor.node"], "anchor.web": web["anchor.web"]},
map[string][]byte{"anchor.node": shell["anchor.node"], "anchor.shell": shell["anchor.shell"]},
"create anchor.web", map[string][]byte{"anchor.node": shell["anchor.node"], "anchor.shell": shell["anchor.shell"], "anchor.web": web["anchor.web"]}, nil, copiedSaid{Copied: 1, Present: 1}},
{"a key that fits no form is left and counted",
map[string][]byte{"anchor.node": web["anchor.node"], "anchor.web": web["anchor.web"], "a.b.c": []byte("x")},
nil, "create anchor.web create anchor.node", web, []string{"a.b.c"}, copiedSaid{Copied: 2, Skipped: 1}},
{"an empty old state copies nothing and says nothing", nil, nil, "", nil, nil, copiedSaid{}},
} {
t.Run(c.name, func(t *testing.T) {
next := &revKV{values: map[string][]byte{}, revs: map[string]uint64{}}
for k, v := range c.present {
next.write(k, v)
}
old := &retiredKV{values: map[string][]byte{}}
maps.Copy(old.values, c.old)
var logs []string
bus := &emitted{}
p := &passer{w: newWorld(), kvs: kvSet{retiredBucket: old, plc.AssignmentsBucket: next}, now: time.Now, seat: &seatActs{bus: bus},
logf: func(f string, a ...any) { logs = append(logs, fmt.Sprintf(f, a...)) }}
if !p.copyRetired(ctx) || !p.copied {
t.Fatalf("the copy did not end: %v", logs)
}
if got := strings.Join(next.log, " "); got != c.wantLog {
t.Errorf("writes = %q, want %q", got, c.wantLog)
}
if c.wantValues != nil && !maps.EqualFunc(next.values, c.wantValues, slices.Equal[[]byte]) {
t.Errorf("assignments = %v, want %v", next.values, c.wantValues)
}
if left := slices.Sorted(maps.Keys(old.values)); !slices.Equal(left, c.wantLeft) && (len(left) != 0 || len(c.wantLeft) != 0) {
t.Errorf("the retired state still holds %v, want %v", left, c.wantLeft)
}
if c.said == (copiedSaid{}) {
if len(bus.events) != 0 || len(logs) != 0 {
t.Errorf("a copy of nothing said %v, %v", bus.events, logs)
}
} else if len(bus.events) != 1 || bus.events[0] != c.said || len(logs) != 1 {
t.Errorf("said %v (journal %v), want the event %+v once", bus.events, logs, c.said)
}
// A restart finds the retired state emptied: nothing comes back that assignments has dropped since.
before := len(next.log)
p.copied = false
if !p.copyRetired(ctx) || len(next.log) != before || len(bus.events) > 1 {
t.Errorf("a second copy wrote %v, said %v", next.log[before:], bus.events)
}
// The copied keys are in the world at once, so the seat's next step reads them.
if got := len(p.w.read(plc.AssignmentsBucket)); got != c.said.Copied {
t.Errorf("%d copied keys are in the world, want %d", got, c.said.Copied)
}
})
}
})
t.Run("1954/D1: a failed provision is the assigner's finding until it is provided", func(t *testing.T) { t.Run("1954/D1: a failed provision is the assigner's finding until it is provided", func(t *testing.T) {
findings := map[string][]byte{} findings := map[string][]byte{}
p := &passer{w: newWorld(), logf: func(string, ...any) {}, kvs: kvSet{buckets.Provisions: nil}, findings: assign.Findings{ p := &passer{w: newWorld(), logf: func(string, ...any) {}, kvs: kvSet{buckets.Provisions: nil}, findings: assign.Findings{
@@ -547,8 +591,11 @@ func (k *revKV) write(key string, value []byte) uint64 {
} }
func (k *revKV) Create(_ context.Context, key string, value []byte, _ ...jetstream.KVCreateOpt) (uint64, error) { func (k *revKV) Create(_ context.Context, key string, value []byte, _ ...jetstream.KVCreateOpt) (uint64, error) {
if _, ok := k.values[key]; ok || key == k.refuse { if _, ok := k.values[key]; ok {
return 0, errors.New("key exists") return 0, jetstream.ErrKeyExists
}
if key == k.refuse {
return 0, errors.New("refused")
} }
k.log = append(k.log, "create "+key) k.log = append(k.log, "create "+key)
return k.write(key, value), nil return k.write(key, value), nil
@@ -581,3 +628,71 @@ type historyStore struct {
func (s historyStore) history(_, key string) ([]stdio.StateRevision, error) { func (s historyStore) history(_, key string) ([]stdio.StateRevision, error) {
return s.revisions[key], nil return s.revisions[key], nil
} }
// putAssignments takes a node's assignments into the world as the keys of the state: a key per module, then the node key.
func putAssignments(w *world, a mma.Assignments) {
values, err := mmac.AssignmentsValues(a)
if err != nil {
panic(err)
}
for _, k := range slices.Sorted(maps.Keys(values)) {
w.putEntry(plc.AssignmentsBucket, k, values[k], 1, time.Now())
}
}
// setAssignments holds a node's assignments in a state as its keys: a key per module, and the node key.
func setAssignments(kv *memKV, a mma.Assignments) {
values, err := mmac.AssignmentsValues(a)
if err != nil {
panic(err)
}
maps.Copy(kv.values, values)
}
// retiredKV is the retired state: its keys, its values, and deletes of them.
type retiredKV struct {
jetstream.KeyValue
values map[string][]byte
}
func (k *retiredKV) Keys(context.Context, ...jetstream.WatchOpt) ([]string, error) {
if len(k.values) == 0 {
return nil, jetstream.ErrNoKeysFound
}
return slices.Sorted(maps.Keys(k.values)), nil
}
func (k *retiredKV) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
v, ok := k.values[key]
if !ok {
return nil, jetstream.ErrKeyNotFound
}
return retiredEntry{key: key, value: v}, nil
}
func (k *retiredKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
delete(k.values, key)
return nil
}
type retiredEntry struct {
jetstream.KeyValueEntry
key string
value []byte
}
func (e retiredEntry) Value() []byte { return e.value }
func (e retiredEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut }
// emitted is a bus that keeps the module events it was asked to say.
type emitted struct {
caller
events []copiedSaid
}
func (e *emitted) Emit(eventType string, body any) error {
if eventType == CopiedEvent {
e.events = append(e.events, body.(copiedSaid))
}
return nil
}
@@ -3,16 +3,16 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"slices" "slices"
"time" "time"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
@@ -23,11 +23,11 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// marksBucket is the assigner's state of what its views family said each state up to. // marksBucket is the assigner's state of what its views family said each state up to.
var marksBucket = placement.Module + "_" + views.MarksState var marksBucket = assignment.Module + "_" + views.MarksState
// viewedTool is one tool over one state of the assigner's. // viewedTool is one tool over one state of the assigner's.
type viewedTool struct{ tool, state, words string } type viewedTool struct{ tool, state, words string }
@@ -35,14 +35,14 @@ type viewedTool struct{ tool, state, words string }
// firstViewTools are the assigner's first-view tools (ADR 0621 §1): each answers only its callers list // firstViewTools are the assigner's first-view tools (ADR 0621 §1): each answers only its callers list
// (views.CallersEnv), which the manifest writes. // (views.CallersEnv), which the manifest writes.
var firstViewTools = []viewedTool{ var firstViewTools = []viewedTool{
{"placement", mma.StatePlacement, "where each module is placed: the seat mesh-module-assigner's placement, by module."}, {"assignments", mma.StateAssignments, "where each module is placed: the seat mesh-module-assigner's assignments, by module."},
{"seat-holders", mma.StateSeatHolders, "who holds which seat on which node."}, {"seat-holders", mma.StateSeatHolders, "who holds which seat on which node."},
{"wants", "wants", "what each node should run: want.<node>.<module>, its fingerprint and the rule that gave it."}, {"wants", "wants", "what each node should run: want.<node>.<module>, its fingerprint and the rule that gave it."},
} }
// readTools are the assigner's ordinary read tools, one per state its readers read (design 25 § 12.2). // readTools are the assigner's ordinary read tools, one per state its readers read (design 25 § 12.2).
var readTools = []viewedTool{ var readTools = []viewedTool{
{"assigner_read_placement", mma.StatePlacement, "where each module is placed, by module."}, {"assigner_read_assignments", mma.StateAssignments, "where each module is placed, by module."},
{"assigner_read_seat_holders", mma.StateSeatHolders, "who holds which seat on which node."}, {"assigner_read_seat_holders", mma.StateSeatHolders, "who holds which seat on which node."},
{"assigner_read_wants", "wants", "what each node should run: want.<node>.<module>."}, {"assigner_read_wants", "wants", "what each node should run: want.<node>.<module>."},
{"assigner_read_findings", mcc.FindingsState, "the assigner's findings, which the condition keeper raises and clears."}, {"assigner_read_findings", mcc.FindingsState, "the assigner's findings, which the condition keeper raises and clears."},
@@ -52,7 +52,7 @@ var readTools = []viewedTool{
// viewedStates are the states the views family says, each by its bucket: the six its readers read. // viewedStates are the states the views family says, each by its bucket: the six its readers read.
var viewedStates = map[string]string{ var viewedStates = map[string]string{
mma.StatePlacement: plc.PlacementBucket, mma.StateAssignments: plc.AssignmentsBucket,
mma.StateSeatHolders: plc.SeatHoldersBucket, mma.StateSeatHolders: plc.SeatHoldersBucket,
"wants": buckets.Wants, "wants": buckets.Wants,
mcc.FindingsState: assign.FindingsBucket, mcc.FindingsState: assign.FindingsBucket,
@@ -66,13 +66,13 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
marks := state(views.MarksState) marks := state(views.MarksState)
var out []stdio.Tool var out []stdio.Tool
for _, t := range firstViewTools { for _, t := range firstViewTools {
out = append(out, views.FirstViewTool(placement.Module, t.tool, t.state, t.words, views.ParseCallers(getenv(views.CallersEnv(t.tool))), state(t.state), marks, now)) out = append(out, views.FirstViewTool(assignment.Module, t.tool, t.state, t.words, views.ParseCallers(getenv(views.CallersEnv(t.tool))), state(t.state), marks, now))
} }
for _, t := range readTools { for _, t := range readTools {
out = append(out, views.ReadTool(placement.Module, t.tool, t.state, t.words, state(t.state), marks, now)) out = append(out, views.ReadTool(assignment.Module, t.tool, t.state, t.words, state(t.state), marks, now))
} }
// Its own first views, of the indexer's manifests, modules and graph (DLV-F3r, ADR 0631 §2): read, and asked again by hand. // Its own first views, of the indexer's manifests, modules and graph (DLV-F3r, ADR 0631 §2): read, and asked again by hand.
return append(out, views.StatusTools(placement.Module, "assigner_first_views", "assigner_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...) return append(out, views.StatusTools(assignment.Module, "assigner_first_views", "assigner_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...)
} }
func keptSource(name string) views.Source { return views.KeptSource{State: stdio.State(name)} } func keptSource(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }
@@ -84,13 +84,13 @@ func startViews(ctx context.Context, js jetstream.JetStream, nc process.Subscrib
if findings.Put == nil { if findings.Put == nil {
return return
} }
if err := viewFindings(findings).Put(views.TooLargeFinding(placement.Module, state, key, size, time.Now())); err != nil { if err := viewFindings(findings).Put(views.TooLargeFinding(assignment.Module, state, key, size, time.Now())); err != nil {
logf("views: the finding of %s.%s too large is not written: %v", state, key, err) logf("views: the finding of %s.%s too large is not written: %v", state, key, err)
} }
} }
process.AwaitBucket(ctx, js, nc, marksBucket, "no change is said on the views family, so every reader waits on its first view", logf).Ready(func(mk jetstream.KeyValue) { process.AwaitBucket(ctx, js, nc, marksBucket, "no change is said on the views family, so every reader waits on its first view", logf).Ready(func(mk jetstream.KeyValue) {
em := viewsEmitter(js, account, mk, findings, tooLarge, logf) em := viewsEmitter(js, account, mk, findings, tooLarge, logf)
for _, state := range []string{"asks", "differences", mcc.FindingsState, mma.StatePlacement, mma.StateSeatHolders, "wants"} { for _, state := range []string{"asks", "differences", mcc.FindingsState, mma.StateAssignments, mma.StateSeatHolders, "wants"} {
state := state state := state
start := func(kv jetstream.KeyValue) { start := func(kv jetstream.KeyValue) {
if err := em.Start(ctx, state, kv, errs); err != nil { if err := em.Start(ctx, state, kv, errs); err != nil {
@@ -109,7 +109,7 @@ func startViews(ctx context.Context, js jetstream.JetStream, nc process.Subscrib
// viewsEmitter is the assigner's emitter of its views family: a change not said past views.Bound is its finding // viewsEmitter is the assigner's emitter of its views family: a change not said past views.Bound is its finding
// view-unsaid, in its findings (review round 2); with no findings state it is said in the log only. // view-unsaid, in its findings (review round 2); with no findings state it is said in the log only.
func viewsEmitter(js jetstream.JetStream, account process.Account, mk jetstream.KeyValue, findings assign.Findings, tooLarge func(string, string, int), logf func(string, ...any)) views.Emitter { func viewsEmitter(js jetstream.JetStream, account process.Account, mk jetstream.KeyValue, findings assign.Findings, tooLarge func(string, string, int), logf func(string, ...any)) views.Emitter {
em := views.Emitter{Owner: placement.Module, Subject: account.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, TooLarge: tooLarge, Logf: logf} em := views.Emitter{Owner: assignment.Module, Subject: account.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, TooLarge: tooLarge, Logf: logf}
if findings.Put != nil && findings.Delete != nil { if findings.Put != nil && findings.Delete != nil {
em.Findings = viewFindings(findings) em.Findings = viewFindings(findings)
} }
@@ -122,7 +122,7 @@ func viewFindings(f assign.Findings) views.FindingWriter {
} }
// firstViewsBucket is the assigner's own state of its first views (views.StatusState). // firstViewsBucket is the assigner's own state of its first views (views.StatusState).
var firstViewsBucket = placement.Module + "_" + views.StatusState var firstViewsBucket = assignment.Module + "_" + views.StatusState
// indexerViews are the assigner's first views of the indexer's manifests, modules and module graph (ADR 0615 §1, // indexerViews are the assigner's first views of the indexer's manifests, modules and module graph (ADR 0615 §1,
// ADR 0621 §2, ADR 0631; DLV-F2r): asked at the start through the indexer's tools, then followed on indexer-views, asking once more on a gap; // ADR 0621 §2, ADR 0631; DLV-F2r): asked at the start through the indexer's tools, then followed on indexer-views, asking once more on a gap;
@@ -154,7 +154,7 @@ func (iv *indexerViews) start(ctx context.Context, w *world, wake func(), settle
if iv.findings.Put == nil { if iv.findings.Put == nil {
return return
} }
f := views.UnansweredFinding(placement.Module, s, time.Now()) f := views.UnansweredFinding(assignment.Module, s, time.Now())
var err error var err error
if raised { if raised {
err = viewFindings(iv.findings).Put(f) err = viewFindings(iv.findings).Put(f)
@@ -209,7 +209,7 @@ func (iv *indexerViews) start(ctx context.Context, w *world, wake func(), settle
// notHeardWriter writes the assigner's finding of a family it does not hear into its findings (heard.Writer); with no // notHeardWriter writes the assigner's finding of a family it does not hear into its findings (heard.Writer); with no
// findings state it is said in the log alone. // findings state it is said in the log alone.
func notHeardWriter(findings assign.Findings, logf func(string, ...any)) heard.Writer { func notHeardWriter(findings assign.Findings, logf func(string, ...any)) heard.Writer {
w := heard.Writer{Observer: placement.Module, Logf: logf, Put: findings.Put} w := heard.Writer{Observer: assignment.Module, Logf: logf, Put: findings.Put}
if findings.Delete != nil { if findings.Delete != nil {
w.Delete = func(key string) error { w.Delete = func(key string) error {
if keys, err := findings.Keys(); err == nil && !slices.Contains(keys, key) { if keys, err := findings.Keys(); err == nil && !slices.Contains(keys, key) {
@@ -12,9 +12,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshhandacts "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" meshhandacts "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
@@ -13,9 +13,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
wdc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" wdc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-delivery/sdk/trunk"
@@ -23,12 +23,12 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assign"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// world is what the assigner watched, as it stands now: every value by bucket and key, and the manifests read by // world is what the assigner watched, as it stands now: every value by bucket and key, and the manifests read by
@@ -45,9 +45,9 @@ type world struct {
meta map[string]entryMeta meta map[string]entryMeta
// trunkBuilt is, by module, its newest done trunk build in the builder's log (putBuilt). // trunkBuilt is, by module, its newest done trunk build in the builder's log (putBuilt).
trunkBuilt map[string]builds.BuildLogEntry trunkBuilt map[string]builds.BuildLogEntry
// placement is the seat's state placement assembled node by node from its keys in either form (mesh-sdk // assignments is the seat's state assignments assembled node by node from its keys in either form (mesh-sdk
// mmac.Keys; research 072 row 37): the one reader of its keys in the assigner. // mmac.Keys; research 072 row 37): the one reader of its keys in the assigner.
placement *mmac.Keys assignmentKeys *mmac.Keys
// sourceGroups is, by revision of the gate's source.main, the release (group) that moved it, from every revision its // sourceGroups is, by revision of the gate's source.main, the release (group) that moved it, from every revision its
// history keeps (watched with history): a build's source revision names the release that carries it (ADR 0601 §1). // history keeps (watched with history): a build's source revision names the release that carries it (ADR 0601 §1).
sourceGroups map[uint64]string sourceGroups map[uint64]string
@@ -68,7 +68,7 @@ type parsed struct {
func newWorld() *world { func newWorld() *world {
return &world{values: map[string]map[string][]byte{}, manifests: map[string]parsed{}, healthy: map[string][]string{}, changes: map[string]uint64{}, return &world{values: map[string]map[string][]byte{}, manifests: map[string]parsed{}, healthy: map[string][]string{}, changes: map[string]uint64{},
meta: map[string]entryMeta{}, trunkBuilt: map[string]builds.BuildLogEntry{}, placement: mmac.NewKeys(), meta: map[string]entryMeta{}, trunkBuilt: map[string]builds.BuildLogEntry{}, assignmentKeys: mmac.NewKeys(),
sourceGroups: map[uint64]string{}} sourceGroups: map[uint64]string{}}
} }
@@ -82,8 +82,8 @@ func (w *world) putEntry(bucket, key string, value []byte, revision uint64, at t
return return
} }
w.meta[bucket+"|"+key] = entryMeta{revision: revision, at: at} w.meta[bucket+"|"+key] = entryMeta{revision: revision, at: at}
if bucket == plc.PlacementBucket { if bucket == plc.AssignmentsBucket {
_, _ = w.placement.Put(key, value, revision, false) // put took it already; this is its revision _, _ = w.assignmentKeys.Put(key, value, revision, false) // put took it already; this is its revision
} }
if bucket == buckets.Source && key == mdc.SourceMainKey { if bucket == buckets.Source && key == mdc.SourceMainKey {
if s, err := mdc.DecodeSource(value); err == nil && s.Group != "" { if s, err := mdc.DecodeSource(value); err == nil && s.Group != "" {
@@ -142,16 +142,16 @@ func (w *world) put(bucket, key string, value []byte) {
w.changes[bucket]++ // the bucket's generation (bucketGen), a delete included w.changes[bucket]++ // the bucket's generation (bucketGen), a delete included
if value == nil { if value == nil {
delete(w.values[bucket], key) delete(w.values[bucket], key)
if bucket == plc.PlacementBucket { if bucket == plc.AssignmentsBucket {
_, _ = w.placement.Put(key, nil, 0, true) _, _ = w.assignmentKeys.Put(key, nil, 0, true)
} }
return return
} }
w.values[bucket][key] = value w.values[bucket][key] = value
w.changes[bucket+"|"+key]++ w.changes[bucket+"|"+key]++
if bucket == plc.PlacementBucket { if bucket == plc.AssignmentsBucket {
// A key or value that does not read places nothing new on its node; the indexer's read tool says it. // A key or value that does not read places nothing new on its node; the indexer's read tool says it.
_, _ = w.placement.Put(key, value, 0, false) _, _ = w.assignmentKeys.Put(key, value, 0, false)
} }
if bucket == buckets.Have { if bucket == buckets.Have {
if h, err := nodeengineclient.DecodeHave(value); err == nil && h.Fp != "" && h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK { if h, err := nodeengineclient.DecodeHave(value); err == nil && h.Fp != "" && h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK {
@@ -256,8 +256,8 @@ func (w *world) manifest(ctx context.Context, get getter, digest string) parsed
return p return p
} }
// assigned is each node's modules the seat mesh-module-assigner's placement names, in name order (ADR 0549 §5, ADR 0567 §8): the // assigned is each node's modules the seat mesh-module-assigner's assignments names, in name order (ADR 0549 §5, ADR 0567 §8): the
// placement is the one source of where modules run. The controller's assignments are read no more (issue 774). // assignments is the one source of where modules run. The controller's assignments are read no more (issue 774).
func (w *world) assigned() map[string][]string { func (w *world) assigned() map[string][]string {
placed, _ := w.placed() placed, _ := w.placed()
out := make(map[string][]string, len(placed)) out := make(map[string][]string, len(placed))
@@ -267,23 +267,23 @@ func (w *world) assigned() map[string][]string {
return out return out
} }
// placed is each node's modules in the seat's placement (ADR 0567 §1), and whether any placement key was read: a key // placed is each node's modules in the seat's assignments (ADR 0567 §1), and whether any assignments key was read: a key
// that does not read places nothing on its node and is said by the indexer's own read tool. // that does not read places nothing on its node and is said by the indexer's own read tool.
func (w *world) placed() (map[string][]string, bool) { func (w *world) placed() (map[string][]string, bool) {
out := map[string][]string{} out := map[string][]string{}
for node, p := range w.placements() { for node, p := range w.assignments() {
out[node] = mmac.Assigned(p) out[node] = mmac.Assigned(p)
} }
return out, len(w.read(plc.PlacementBucket)) > 0 return out, len(w.read(plc.AssignmentsBucket)) > 0
} }
// placements is every node's placement, assembled from its keys in either form (mmac.Keys). // assignments is every node's assignments, assembled from its keys in either form (mmac.Keys).
func (w *world) placements() map[string]mma.Placement { func (w *world) assignments() map[string]mma.Assignments {
out, _ := w.placement.AllRevisions() out, _ := w.assignmentKeys.AllRevisions()
return out return out
} }
// placedNodes is every node the seat's placement names (ADR 0567 §1): those compose their own modules (ADR 0547 §2), // placedNodes is every node the seat's assignments names (ADR 0567 §1): those compose their own modules (ADR 0547 §2),
// so their wants name the build alone and the seventh rule does not hold them. // so their wants name the build alone and the seventh rule does not hold them.
func (w *world) placedNodes() map[string]bool { func (w *world) placedNodes() map[string]bool {
placed, _ := w.placed() placed, _ := w.placed()
@@ -309,8 +309,8 @@ func (w *world) trunk() map[string]meshcatalog.ModulesByModuleValue {
return out return out
} }
// records is each node's record in its own have key (`<node>.node`): the nodes a placement proposal is answered by, // records is each node's record in its own have key (`<node>.node`): the nodes an assignments proposal is answered by,
// with those placement names (ADR 0593 §8). // with those assignments names (ADR 0593 §8).
func (w *world) records() map[string]nodeengine.NodeRecord { func (w *world) records() map[string]nodeengine.NodeRecord {
out := map[string]nodeengine.NodeRecord{} out := map[string]nodeengine.NodeRecord{}
for key, value := range w.read(buckets.Have) { for key, value := range w.read(buckets.Have) {
@@ -542,7 +542,7 @@ func (w *world) ownComposed(node string) (assign.ComposedDigest, bool) {
Words: "the node composes its own"}, true Words: "the node composes its own"}, true
} }
// unplacedNodes are the nodes whose engine has a have and that no placement names: they get no // unplacedNodes are the nodes whose engine has a have and that no assignments names: they get no
// want, since nothing composes them. // want, since nothing composes them.
func (w *world) unplacedNodes() []string { func (w *world) unplacedNodes() []string {
placed := w.placedNodes() placed := w.placedNodes()
+4 -4
View File
@@ -4,9 +4,9 @@ go 1.26.0
require ( require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
@@ -65,7 +65,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks =>
require git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate => ../mesh-delivery-view/viewstate replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate => ../mesh-delivery-view/viewstate
require git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000
require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
replace git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient => ../../contracts/go/clients/meshdeliverycheckerclient replace git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient => ../../contracts/go/clients/meshdeliverycheckerclient
@@ -2,14 +2,14 @@ package assign
import ( import (
"fmt" "fmt"
"mesh-delivery-assigner/internal/placement" "mesh-delivery-assigner/internal/assignment"
"regexp" "regexp"
"strings" "strings"
"time" "time"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
@@ -69,7 +69,7 @@ func RestoreAsk(key, node, module, fp, words string, now time.Time) asks.Ask {
// words, under the assigner's own name (ADR 0437 clause 9), keyed by the held fingerprint. needsYou is true for a module holding // words, under the assigner's own name (ADR 0437 clause 9), keyed by the held fingerprint. needsYou is true for a module holding
// data, whose restore ask waits for the operator. // data, whose restore ask waits for the operator.
func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Condition { func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Condition {
key := placement.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2) key := assignment.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised { if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared} return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
@@ -92,7 +92,7 @@ func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Con
// the node keeps its want. It names the node, the module, fp and the have's words, keyed by node and module, and is cleared // the node keeps its want. It names the node, the module, fp and the have's words, keyed by node and module, and is cleared
// (raised false) once the node no longer fails alone. // (raised false) once the node no longer fails alone.
func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc.Condition { func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc.Condition {
key := placement.Module + ".failed-alone." + node + "." + module key := assignment.Module + ".failed-alone." + node + "." + module
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised { if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
@@ -115,7 +115,7 @@ func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc
// naming the module, the node and the rule holding its first want back with the rule's words, under the assigner's own name keyed by // naming the module, the node and the rule holding its first want back with the rule's words, under the assigner's own name keyed by
// node and module, and cleared (raised false) when the node has a want for it again. // node and module, and cleared (raised false) when the node has a want for it again.
func NeitherCondition(node, module string, h Holding, raised bool) oc.Condition { func NeitherCondition(node, module string, h Holding, raised bool) oc.Condition {
key := placement.Module + ".neither." + node + "." + module key := assignment.Module + ".neither." + node + "." + module
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised { if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
@@ -237,7 +237,7 @@ func BackupAsk(key, node, module, fp, digest, words string, asking int, now time
// BackupCondition is the condition raised when a backup before an apply fails and the module is held on the node (ADR 0539 §16), // BackupCondition is the condition raised when a backup before an apply fails and the module is held on the node (ADR 0539 §16),
// naming the node, the module and the backup's words; cleared under the same key. // naming the node, the module and the backup's words; cleared under the same key.
func BackupCondition(node, module, fp, words string, raised bool) oc.Condition { func BackupCondition(node, module, fp, words string, raised bool) oc.Condition {
key := placement.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2) key := assignment.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
if !raised { if !raised {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared} return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
@@ -2,16 +2,16 @@ package assign
import ( import (
"fmt" "fmt"
"mesh-delivery-assigner/internal/placement" "mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/puts" "mesh-delivery-assigner/internal/puts"
"slices" "slices"
"strings" "strings"
"testing" "testing"
"time" "time"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
@@ -148,7 +148,7 @@ func saidConditionsAreFindings(t *testing.T) {
{HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".held"}, {HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".held"},
{NodeFailedCondition("ace", "mail", fNew, NodeFailure{Words: "x", Others: []string{"g14"}}, true), NodeFailedCondition("ace", "mail", fNew, NodeFailure{}, false), "module.ace.mail.failed-alone"}, {NodeFailedCondition("ace", "mail", fNew, NodeFailure{Words: "x", Others: []string{"g14"}}, true), NodeFailedCondition("ace", "mail", fNew, NodeFailure{}, false), "module.ace.mail.failed-alone"},
{NeitherCondition("ace", "mail", Holding{Rule: 3, Words: "w"}, true), NeitherCondition("ace", "mail", Holding{}, false), "module.ace.mail.neither"}, {NeitherCondition("ace", "mail", Holding{Rule: 3, Words: "w"}, true), NeitherCondition("ace", "mail", Holding{}, false), "module.ace.mail.neither"},
{SeatWaitsCondition([]string{"placement"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"}, {SeatWaitsCondition([]string{"assignments"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
{BackupCondition("ace", "mail", fNew, "disk full", true), BackupCondition("ace", "mail", fNew, "", false), "module.ace.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".backup"}, {BackupCondition("ace", "mail", fNew, "disk full", true), BackupCondition("ace", "mail", fNew, "", false), "module.ace.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".backup"},
} { } {
puts, deletes, said := map[string]mcseat.FindingsByKeyValue{}, []string{}, 0 puts, deletes, said := map[string]mcseat.FindingsByKeyValue{}, []string{}, 0
@@ -162,7 +162,7 @@ func saidConditionsAreFindings(t *testing.T) {
t.Fatalf("%s: %v", c.key, err) t.Fatalf("%s: %v", c.key, err)
} }
got, ok := puts[c.key] got, ok := puts[c.key]
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != placement.Module || if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != assignment.Module ||
got.Subject.Machine != c.raised.Subject.Machine || said != 0 { got.Subject.Machine != c.raised.Subject.Machine || said != 0 {
t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said) t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said)
} }
@@ -1,7 +1,7 @@
package assign package assign
import ( import (
"mesh-delivery-assigner/internal/placement" "mesh-delivery-assigner/internal/assignment"
"strings" "strings"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
@@ -11,8 +11,8 @@ import (
// SeatWaitsCondition is the assigner's word on the seat mesh-module-assigner while a state of the seat is not bound (ADR 0567 // SeatWaitsCondition is the assigner's word on the seat mesh-module-assigner while a state of the seat is not bound (ADR 0567
// §1): raised naming each, cleared when none is left. // §1): raised naming each, cleared when none is left.
func SeatWaitsCondition(unbound []string) oc.Condition { func SeatWaitsCondition(unbound []string) oc.Condition {
key := placement.Module + ".seat-states" key := assignment.Module + ".seat-states"
subject := oc.ConditionSubject{Scope: "module", ID: placement.Module} subject := oc.ConditionSubject{Scope: "module", ID: assignment.Module}
if len(unbound) == 0 { if len(unbound) == 0 {
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: "the seat mesh-module-assigner holds all its states."} return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: "the seat mesh-module-assigner holds all its states."}
} }
@@ -3,7 +3,7 @@ package assign
import ( import (
"errors" "errors"
"fmt" "fmt"
"mesh-delivery-assigner/internal/placement" "mesh-delivery-assigner/internal/assignment"
"strings" "strings"
"time" "time"
@@ -21,13 +21,13 @@ import (
// assigner no longer says it straight (never both). // assigner no longer says it straight (never both).
// FindingsBucket is the assigner's findings. // FindingsBucket is the assigner's findings.
var FindingsBucket = mcc.FindingsBucket(placement.Module) var FindingsBucket = mcc.FindingsBucket(assignment.Module)
// FindingOf is a said condition as a finding: its key `<scope>.<id>.<kind>`, from the condition's own key // FindingOf is a said condition as a finding: its key `<scope>.<id>.<kind>`, from the condition's own key
// `mesh-delivery-assigner.<kind>.<id…>` (the subject's id when the key names none), its words unchanged. raised is false for // `mesh-delivery-assigner.<kind>.<id…>` (the subject's id when the key names none), its words unchanged. raised is false for
// a clear, whose key is to be deleted; value is nil then. // a clear, whose key is to be deleted; value is nil then.
func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised bool, err error) { func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised bool, err error) {
rest, ok := strings.CutPrefix(c.Key, placement.Module+".") rest, ok := strings.CutPrefix(c.Key, assignment.Module+".")
if !ok || rest == "" { if !ok || rest == "" {
return "", nil, false, fmt.Errorf("the condition %q is not the assigner's", c.Key) return "", nil, false, fmt.Errorf("the condition %q is not the assigner's", c.Key)
} }
@@ -56,7 +56,7 @@ func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised b
} }
f := mcc.Observation{Scope: scope, ID: id, Kind: kind, Machine: c.Subject.Machine, Severity: string(c.Severity), Summary: summary, f := mcc.Observation{Scope: scope, ID: id, Kind: kind, Machine: c.Subject.Machine, Severity: string(c.Severity), Summary: summary,
Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: actions, Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: actions,
Source: "assigner." + kind}.Finding(placement.Module, "", at) Source: "assigner." + kind}.Finding(assignment.Module, "", at)
value, err = mcc.EncodeFinding(f) value, err = mcc.EncodeFinding(f)
return key, value, true, err return key, value, true, err
} }
@@ -3,8 +3,8 @@ package assign
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mesh-delivery-assigner/internal/assignment"
"mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/placement"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -20,7 +20,7 @@ import (
// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus as any module (ADR 0601 §1). // catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus as any module (ADR 0601 §1).
// PoliciesBucket is the assigner's state `policies`. // PoliciesBucket is the assigner's state `policies`.
var PoliciesBucket = placement.Module + "_" + buckets.PoliciesState var PoliciesBucket = assignment.Module + "_" + buckets.PoliciesState
// DecodePolicies reads the policies state's values by key (the module); a value that cannot be read is left out, named. // DecodePolicies reads the policies state's values by key (the module); a value that cannot be read is left out, named.
func DecodePolicies(values map[string][]byte) (map[string]catalogue.PersonPolicy, []string) { func DecodePolicies(values map[string][]byte) (map[string]catalogue.PersonPolicy, []string) {
@@ -76,7 +76,7 @@ func ProvisionFailedFinding(key string, p meshprovisionerclient.Provision, at ti
// encode is the observation as the assigner's finding at at, in the seat's layout. // encode is the observation as the assigner's finding at at, in the seat's layout.
func encode(o mcc.Observation, at time.Time) ([]byte, error) { func encode(o mcc.Observation, at time.Time) ([]byte, error) {
return mcc.EncodeFinding(o.Finding(placement.Module, "", at)) return mcc.EncodeFinding(o.Finding(assignment.Module, "", at))
} }
func also(provider, consumer string) []string { func also(provider, consumer string) []string {
@@ -109,7 +109,7 @@ func ProvisionMissingFinding(key string, bound time.Duration, at time.Time) ([]b
Resolved: provision + " for " + module + " on " + node + " has its key", Source: "assigner." + KindProvisionMissing}, at) Resolved: provision + " for " + module + " on " + node + " has its key", Source: "assigner." + KindProvisionMissing}, at)
} }
// KindUnplaced is, from the lease write, a node that runs but no placement names (ADR 0571 §6). // KindUnplaced is, from the lease write, a node that runs but no assignments names (ADR 0571 §6).
const KindUnplaced = "unplaced" const KindUnplaced = "unplaced"
// UnplacedKey is its finding's key, `machine.<node>.unplaced`. // UnplacedKey is its finding's key, `machine.<node>.unplaced`.
@@ -117,12 +117,12 @@ func UnplacedKey(node string) string {
return mcc.Key(mcc.ScopeMachine, node, KindUnplaced) return mcc.Key(mcc.ScopeMachine, node, KindUnplaced)
} }
// UnplacedFinding is the finding of a node no placement names since the lease write. // UnplacedFinding is the finding of a node no assignments names since the lease write.
func UnplacedFinding(node string, at time.Time) ([]byte, error) { func UnplacedFinding(node string, at time.Time) ([]byte, error) {
summary := fmt.Sprintf("%s runs, but no placement names it: since the controller handed its lease away nothing composes it, "+ summary := fmt.Sprintf("%s runs, but no assignments names it: since the controller handed its lease away nothing composes it, "+
"so the assigner writes it no want", node) "so the assigner writes it no want", node)
return encode(mcc.Observation{Scope: mcc.ScopeMachine, ID: node, Kind: KindUnplaced, Machine: node, Summary: summary, return encode(mcc.Observation{Scope: mcc.ScopeMachine, ID: node, Kind: KindUnplaced, Machine: node, Summary: summary,
Headline: clipHeadline(node + " has no placement"), Needs: "place it with mesh-module-assigner.propose.", Headline: clipHeadline(node + " has no assignments"), Needs: "place it with mesh-module-assigner.propose.",
Explanation: occ.NeedsYou + "place it with mesh-module-assigner.propose. " + summary + ".", Explanation: occ.NeedsYou + "place it with mesh-module-assigner.propose. " + summary + ".",
Resolved: node + " is placed", Source: "assigner." + KindUnplaced}, at) Resolved: node + " is placed", Source: "assigner." + KindUnplaced}, at)
} }
@@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"mesh-delivery-assigner/internal/puts" "mesh-delivery-assigner/internal/puts"
@@ -75,7 +75,7 @@ func StepPut(w PutWorld) PutNext {
} }
fp := short(p.FP) fp := short(p.FP)
if !w.Assigned { if !w.Assigned {
return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its placement first)", w.Module, w.Node) return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its assignments first)", w.Module, w.Node)
} }
switch w.Build { switch w.Build {
case builds.BuildDone: case builds.BuildDone:
@@ -10,27 +10,27 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"mesh-delivery-assigner/internal/placement" "mesh-delivery-assigner/internal/assignment"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: placement is its state // The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: assignments is its state
// `placement`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the // `assignments`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
// operator's Approve at the revision it was laid over (§5). What here decides; the tools and the passes do. // operator's Approve at the revision it was laid over (§5). What here decides; the tools and the passes do.
// ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current placement and the // ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current assignments and the
// revision of its key; which modules the indexer holds and which nodes have a record; and, for the seed, the export's // revision of its key; which modules the indexer holds and which nodes have a record; and, for the seed, the export's
// digest. // digest.
type ProposeIn struct { type ProposeIn struct {
ID, By, Why string ID, By, Why string
At time.Time At time.Time
Changes []mma.ProposalChange Changes []mma.ProposalChange
Current map[string]mma.Placement Current map[string]mma.Assignments
Revisions map[string]uint64 Revisions map[string]uint64
Held func(module string) bool Held func(module string) bool
Recorded func(node string) bool Recorded func(node string) bool
@@ -39,11 +39,11 @@ type ProposeIn struct {
} }
// Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision), // Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision),
// the laid placement of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what // the laid assignments of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what
// CheckPlacements refuses of the result laid over every placement (two modules on one host port, a provider not placed, // CheckAssignments refuses of the result laid over every assignments (two modules on one host port, a provider not placed,
// a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no // a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no
// record). A key whose value the proposal leaves as it is changes nothing and is not laid. // record). A key whose value the proposal leaves as it is changes nothing and is not laid.
func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) { func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Assignments, []string) {
p := mma.Proposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(), p := mma.Proposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(),
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering} Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
var findings []string var findings []string
@@ -64,13 +64,13 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
laid, lay := plc.LayChanges(in.Current, in.Changes) laid, lay := plc.LayChanges(in.Current, in.Changes)
findings = append(findings, lay...) findings = append(findings, lay...)
findings = append(findings, ControllerRefusals(in.Changes, laid)...) findings = append(findings, ControllerRefusals(in.Changes, laid)...)
all := map[string]mma.Placement{} all := map[string]mma.Assignments{}
for node, pl := range in.Current { for node, pl := range in.Current {
all[node] = pl all[node] = pl
} }
out := map[string]*mma.Placement{} out := map[string]*mma.Assignments{}
for node, pl := range laid { for node, pl := range laid {
if before, ok := in.Current[node]; ok && mmac.SamePlacement(before, pl) { if before, ok := in.Current[node]; ok && mmac.SameAssignments(before, pl) {
continue continue
} }
pl := pl pl := pl
@@ -79,7 +79,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
p.Base[node] = int64(in.Revisions[node]) p.Base[node] = int64(in.Revisions[node])
} }
// A remove-node deletes the key of a joining node that never confirmed (the operator's decision of 2026-11-02 01:05): its // A remove-node deletes the key of a joining node that never confirmed (the operator's decision of 2026-11-02 01:05): its
// candidate is no placement, and the checks across nodes read the mesh without it. LayChanges said why one cannot go. // candidate is no assignments, and the checks across nodes read the mesh without it. LayChanges said why one cannot go.
for _, node := range plc.RemovedNodes(p) { for _, node := range plc.RemovedNodes(p) {
if _, ok := in.Current[node]; !ok { if _, ok := in.Current[node]; !ok {
continue continue
@@ -89,7 +89,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
p.Base[node] = int64(in.Revisions[node]) p.Base[node] = int64(in.Revisions[node])
} }
if len(findings) == 0 && len(out) == 0 { if len(findings) == 0 && len(out) == 0 {
findings = append(findings, "the changes leave every placement as it is") findings = append(findings, "the changes leave every assignments as it is")
} }
held, recorded := in.Held, in.Recorded held, recorded := in.Held, in.Recorded
if held == nil { if held == nil {
@@ -110,7 +110,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
was := recorded was := recorded
recorded = func(node string) bool { return joining[node] || was(node) } recorded = func(node string) bool { return joining[node] || was(node) }
} }
for _, r := range plc.CheckPlacements(all, held, recorded) { for _, r := range plc.CheckAssignments(all, held, recorded) {
findings = append(findings, r) findings = append(findings, r)
} }
return p, out, findings return p, out, findings
@@ -119,14 +119,14 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
// ControllerModule is the controller's module, which nothing places again (issue 1126). // ControllerModule is the controller's module, which nothing places again (issue 1126).
const ControllerModule = "mesh-controller" const ControllerModule = "mesh-controller"
// ControllerEnded is why a placement of the controller is refused: the parts that acted only once its lease was handed // ControllerEnded is why assignments of the controller is refused: the parts that acted only once its lease was handed
// (consumers, the user list, enrolment, dead letters) now act without reading the lease, so a controller placed again // (consumers, the user list, enrolment, dead letters) now act without reading the lease, so a controller placed again
// would write beside them, silently. // would write beside them, silently.
const ControllerEnded = "the controller ended by its unassignment on 2026-11-02; placing it again would make two writers (issue 1126)" const ControllerEnded = "the controller ended by its unassignment on 2026-11-02; placing it again would make two writers (issue 1126)"
// ControllerRefusals names every change, and every node as laid, that places the controller: by a place, by a seed's // ControllerRefusals names every change, and every node as laid, that places the controller: by a place, by a seed's
// whole placement, or in what the changes lay. None when nothing does. // whole assignments, or in what the changes lay. None when nothing does.
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placement) []string { func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Assignments) []string {
var out []string var out []string
nodes := map[string]bool{} nodes := map[string]bool{}
for i, c := range changes { for i, c := range changes {
@@ -148,16 +148,16 @@ func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placem
} }
// Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys, // Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys,
// mesh-sdk mmac.Keys.Writes), the revision of the node's placement it is written at (0: it has none, so // mesh-sdk mmac.Keys.Writes), the revision of the node's assignments it is written at (0: it has none, so
// it is created) and its value (nil: it is deleted). // it is created) and its value (nil: it is deleted).
type Write struct { type Write struct {
Node, Key string Node, Key string
Base uint64 Base uint64
Value *mma.Placement Value *mma.Assignments
} }
// Writes is every key a proposal writes, by node order. // Writes is every key a proposal writes, by node order.
func Writes(p mma.Proposal, laid map[string]*mma.Placement) ([]Write, error) { func Writes(p mma.Proposal, laid map[string]*mma.Assignments) ([]Write, error) {
var out []Write var out []Write
for _, node := range plc.Nodes(p) { for _, node := range plc.Nodes(p) {
key, err := mmac.NodeKey(node) key, err := mmac.NodeKey(node)
@@ -179,40 +179,40 @@ const (
// CallClearedCondition clears the condition of a refused controller call a previous process raised, for its pair (node and // CallClearedCondition clears the condition of a refused controller call a previous process raised, for its pair (node and
// module, or node and provision for a pin). // module, or node and provision for a pin).
func CallClearedCondition(kind, node, module string) oc.Condition { func CallClearedCondition(kind, node, module string) oc.Condition {
return oc.Condition{Key: placement.Module + "." + kind + "." + node + "." + module, return oc.Condition{Key: assignment.Module + "." + kind + "." + node + "." + module,
Subject: oc.ConditionSubject{Scope: "module", ID: module, Machine: node}, State: occ.StateCleared, Subject: oc.ConditionSubject{Scope: "module", ID: module, Machine: node}, State: occ.StateCleared,
Resolved: "the assigner calls no verb of the controller any more: placement is the one source."} Resolved: "the assigner calls no verb of the controller any more: assignments is the one source."}
} }
// PlacementAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions. // AssignmentsAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
func PlacementAct(p mma.Proposal) occ.Act { func AssignmentsAct(p mma.Proposal) occ.Act {
return occ.Act{"placement": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)} return occ.Act{"assignments": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
} }
// PlacementAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is // AssignmentsAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is
// another asking, under an id of its own. // another asking, under an id of its own.
func PlacementAsking(p mma.Proposal, at time.Time) int { func AssignmentsAsking(p mma.Proposal, at time.Time) int {
return int(at.Unix()) return int(at.Unix())
} }
// PlacementAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that // AssignmentsAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that
// proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation. // proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation.
func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) { func AssignmentsAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
binds, err := occ.ActDigest(PlacementAct(p)) binds, err := occ.ActDigest(AssignmentsAct(p))
if err != nil { if err != nil {
return asks.Ask{}, err return asks.Ask{}, err
} }
nodes := plc.Nodes(p) nodes := plc.Nodes(p)
headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", ")) headline := fmt.Sprintf("Change assignments on %s?", strings.Join(nodes, ", "))
if n := len([]rune(headline)); n > occ.HeadlineLength { if n := len([]rune(headline)); n > occ.HeadlineLength {
headline = fmt.Sprintf("Change placement on %d machines?", len(nodes)) headline = fmt.Sprintf("Change assignments on %d machines?", len(nodes))
} }
return assignerAsk(asking, oc.Ask{ return assignerAsk(asking, oc.Ask{
ID: meshdeliveryclient.AskingID(key, asking), ID: meshdeliveryclient.AskingID(key, asking),
Headline: headline, Headline: headline,
Explanation: brief, Explanation: brief,
Options: []oc.Option{ Options: []oc.Option{
{ID: OptionYes, Label: "Approve", Does: "the placement of " + strings.Join(nodes, ", ") + " is written as proposed", {ID: OptionYes, Label: "Approve", Does: "the assignments of " + strings.Join(nodes, ", ") + " is written as proposed",
Level: occ.Approve, Binds: binds}, Level: occ.Approve, Binds: binds},
{ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: occ.Acknowledge}, {ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: occ.Acknowledge},
}, },
@@ -225,7 +225,7 @@ func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time
// Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again // Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again
// over the keys as the write left them (§5: a key moved while it was written). False when nothing is left. // over the keys as the write left them (§5: a key moved while it was written). False when nothing is left.
func Rest(p mma.Proposal, written map[string]*mma.Placement) (mma.Proposal, bool) { func Rest(p mma.Proposal, written map[string]*mma.Assignments) (mma.Proposal, bool) {
rest := p rest := p
rest.Changes, rest.Base = nil, map[string]int64{} rest.Changes, rest.Base = nil, map[string]int64{}
for _, c := range p.Changes { for _, c := range p.Changes {
@@ -8,23 +8,23 @@ import (
"testing" "testing"
"time" "time"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// ADR 0567 §2, §5, §7, §8: what the seat's holder decides of a proposal: every refusal named at once, a valid one laid // ADR 0567 §2, §5, §7, §8: what the seat's holder decides of a proposal: every refusal named at once, a valid one laid
// with the base revision of each key it changes, the writes at those revisions, the controller's calls that keep its // with the base revision of each key it changes, the writes at those revisions, the controller's calls that keep its
// assignments in step, the authorising ask bound to the changes, and the export read as the controller answers it. // assignments in step, the authorising ask bound to the changes, and the export read as the controller answers it.
func TestThePlacementSeatDecides(t *testing.T) { func TestTheAssignmentsSeatDecides(t *testing.T) {
anchor := mma.Placement{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"}, anchor := mma.Assignments{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
Modules: []mma.PlacedModule{{Module: "store", Ports: map[string]int64{"5432": 5432}}, {Module: "web"}}} Modules: []mma.PlacedModule{{Module: "store", Ports: map[string]int64{"5432": 5432}}, {Module: "web"}}}
laptop := mma.Placement{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}} laptop := mma.Assignments{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
in := func(changes ...mma.ProposalChange) ProposeIn { in := func(changes ...mma.ProposalChange) ProposeIn {
return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes, return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes,
Current: map[string]mma.Placement{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9}, Current: map[string]mma.Assignments{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
Held: func(m string) bool { return m != "ghost" }, Held: func(m string) bool { return m != "ghost" },
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }} Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
} }
@@ -43,14 +43,14 @@ func TestThePlacementSeatDecides(t *testing.T) {
}{ }{
"a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"}, "a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
"a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"}, "a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"}, "nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every assignments as it is"},
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"}, "no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"}, "no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
"a seed by hand": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"}, "a seed by hand": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that // 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
// act without its lease. // act without its lease.
"the controller placed again": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"}, "the controller placed again": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor", "the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Assignments{V: keys.Version, Node: "anchor",
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"}, Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
"a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"}, "a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
} { } {
@@ -59,7 +59,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
} }
} }
// Every kind of change is taken: the placement is the one source, and the controller's verbs limit nothing (ADR 0567 §8). // Every kind of change is taken: the assignments is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
for _, c := range []mma.ProposalChange{ for _, c := range []mma.ProposalChange{
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}}, {Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
{Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}}, {Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
@@ -74,7 +74,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
} }
// A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again. // A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again.
if rest, ok := Rest(p, map[string]*mma.Placement{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" || if rest, ok := Rest(p, map[string]*mma.Assignments{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil { len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
t.Errorf("the rest = %+v, %v", rest, ok) t.Errorf("the rest = %+v, %v", rest, ok)
} }
@@ -95,13 +95,13 @@ func TestThePlacementSeatDecides(t *testing.T) {
if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 { if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 {
t.Fatalf("writes = %+v, %v", writes, err) t.Fatalf("writes = %+v, %v", writes, err)
} }
a, err := PlacementAsk("placement.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0)) a, err := AssignmentsAsk("assignments.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], PlacementAct(p)) != nil { if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(p)) != nil {
t.Fatalf("the ask = %+v, %v", a, err) t.Fatalf("the ask = %+v, %v", a, err)
} }
q := p q := p
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"}) q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"})
if occ.Performs(mustAsk(t, a).Options[0], PlacementAct(q)) == nil { if occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(q)) == nil {
t.Error("the ask's yes binds changes it was not asked about") t.Error("the ask's yes binds changes it was not asked about")
} }
} }
@@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"maps" "maps"
"mesh-delivery-assigner/internal/puts" "mesh-delivery-assigner/internal/puts"
@@ -90,9 +90,9 @@ type Module struct {
// needs, ADR 0618 §1): a node's want is held only while that node's engine reads none of them (ADR 0624 §2). Nil: the // needs, ADR 0618 §1): a node's want is held only while that node's engine reads none of them (ADR 0624 §2). Nil: the
// build names none, and holds nothing. // build names none, and holds nothing.
FormNeeds []string FormNeeds []string
// Self says whether node composes the module itself: the catalogue's placement names the node (ADR 0547 §2). It is the one // Self says whether node composes the module itself: the catalogue's assignments names the node (ADR 0547 §2). It is the one
// source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while // source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while
// the node was placed does not outlive the placement (issue 1126). Nil: no node does. // the node was placed does not outlive the assignments (issue 1126). Nil: no node does.
Self func(node string) bool Self func(node string) bool
} }
@@ -587,11 +587,11 @@ func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string]
return out return out
} }
// selfDerived is next with its Self derived from the placement now (Module.Self), whichever rule made it, and whether it is // selfDerived is next with its Self derived from the assignments now (Module.Self), whichever rule made it, and whether it is
// written. A placed node's want names the build alone (ADR 0547 §2). An unplaced node's want that a rule kept from the want // written. A placed node's want names the build alone (ADR 0547 §2). An unplaced node's want that a rule kept from the want
// written while the node was placed (held at F, held back by rule 3 or 5, a data module asked about) names the controller's // written while the node was placed (held at F, held back by rule 3 or 5, a data module asked about) names the controller's
// composition of the same fingerprint, asked as any move of a record module is (ADR 0539 §14): on 2026-10-27, after the // composition of the same fingerprint, asked as any move of a record module is (ADR 0539 §14): on 2026-10-27, after the
// placement seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept // assignments seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
// their Self wants, which their node agents, no longer placed, could not compose (issue 1126). With no such composition, or // their Self wants, which their node agents, no longer placed, could not compose (issue 1126). With no such composition, or
// while its ask waits, the want written stays and the node's holding says why. // while its ask waits, the want written stays and the node's holding says why.
func (m Module) selfDerived(node string, before mwc.Want, had bool, next mwc.Want, n Nodes, out *Outcome) (mwc.Want, bool) { func (m Module) selfDerived(node string, before mwc.Want, had bool, next mwc.Want, n Nodes, out *Outcome) (mwc.Want, bool) {
@@ -3,8 +3,8 @@ package assign
import ( import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
@@ -348,7 +348,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
} }
} }
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names // 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the assignments is gone, names
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient, // the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
// rule 5, kept their Self wants, which g14 could no longer compose); with no such composition, or while a record ask waits, it // rule 5, kept their Self wants, which g14 could no longer compose); with no such composition, or while a record ask waits, it
// stays and is said; a placed node's want is Self with no composition. // stays and is said; a placed node's want is Self with no composition.
@@ -1,4 +1,4 @@
package placement package assignment
import ( import (
"encoding/json" "encoding/json"
@@ -9,16 +9,16 @@ import (
"testing" "testing"
"time" "time"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
) )
// ADR 0567 §2, §4: what a change says, the holder's check of every placement, a proposal's changes laid over the keys, // ADR 0567 §2, §4: what a change says, the holder's check of every assignments, a proposal's changes laid over the keys,
// its digests (held to the values the earlier holder's code made, in the field order it wrote), its refusals, and who holds // its digests (held to the values the earlier holder's code made, in the field order it wrote), its refusals, and who holds
// which seat. // which seat.
func TestPlacement(t *testing.T) { func TestAssignments(t *testing.T) {
g14 := seat.Placement{Node: "g14", Revision: "abc", g14 := seat.Assignments{Node: "g14", Revision: "abc",
Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}, Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"},
Tunnel: &seat.FoundTunnel{Interface: "wg0", Config: "c", Port: 51820, Mtu: 1420, PublicKey: "K", Peers: []seat.FoundTunnelPeer{{PublicKey: "P", Address: "10.0.0.3"}}}, Tunnel: &seat.FoundTunnel{Interface: "wg0", Config: "c", Port: 51820, Mtu: 1420, PublicKey: "K", Peers: []seat.FoundTunnelPeer{{PublicKey: "P", Address: "10.0.0.3"}}},
PeerNames: map[string]string{"P": "pi"}}, PeerNames: map[string]string{"P": "pi"}},
@@ -29,13 +29,13 @@ func TestPlacement(t *testing.T) {
Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}}, Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}},
Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}}, Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
} }
novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}}, novox := seat.Assignments{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
Holds: []seat.Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}} Holds: []seat.Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}}
current := map[string]seat.Placement{"g14": g14, "novox": novox} current := map[string]seat.Assignments{"g14": g14, "novox": novox}
const id = "p-20261028t183000-1a2b3c" const id = "p-20261028t183000-1a2b3c"
// the enroller is whichever module holds the seat mesh-node-enroller: no module is named for it // the enroller is whichever module holds the seat mesh-node-enroller: no module is named for it
const enroller = "enroll-lab" const enroller = "enroll-lab"
enrollers := EnrollersOf(map[string]seat.Placement{"ace": {Node: "ace", enrollers := EnrollersOf(map[string]seat.Assignments{"ace": {Node: "ace",
Holds: []seat.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: enroller}}}}) Holds: []seat.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: enroller}}}})
if !slices.Equal(enrollers, []string{enroller}) { if !slices.Equal(enrollers, []string{enroller}) {
t.Fatalf("the holders of the enroller seat = %v", enrollers) t.Fatalf("the holders of the enroller seat = %v", enrollers)
@@ -54,7 +54,7 @@ func TestPlacement(t *testing.T) {
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) { if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
t.Errorf("a pin change changes every module on the node: %+v", got) t.Errorf("a pin change changes every module on the node: %+v", got)
} }
if got := ChangeOf(seat.Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" { if got := ChangeOf(seat.Assignments{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
t.Errorf("a new node: %+v", got) t.Errorf("a new node: %+v", got)
} }
c := []Change{{Node: "novox"}, {Node: "g14", Before: 1, After: 2}} c := []Change{{Node: "novox"}, {Node: "g14", Before: 1, After: 2}}
@@ -64,18 +64,18 @@ func TestPlacement(t *testing.T) {
} }
}) })
t.Run("the check of every placement", func(t *testing.T) { t.Run("the check of every assignments", func(t *testing.T) {
known := func(m string) bool { return m != "ghost" } known := func(m string) bool { return m != "ghost" }
enrolled := func(n string) bool { return n != "stranger" } enrolled := func(n string) bool { return n != "stranger" }
if r := CheckPlacements(current, known, enrolled); len(r) != 0 { if r := CheckAssignments(current, known, enrolled); len(r) != 0 {
t.Fatalf("a sound placement is refused: %v", r) t.Fatalf("a sound assignments is refused: %v", r)
} }
// A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there. // A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there.
for name, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} { for name, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} {
a := seat.Placement{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}} a := seat.Assignments{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}}
b := a b := a
b.Node = "novox" b.Node = "novox"
r := CheckPlacements(map[string]seat.Placement{"ace": a, "novox": b}, known, enrolled) r := CheckAssignments(map[string]seat.Assignments{"ace": a, "novox": b}, known, enrolled)
if (len(r) != 0) != refused { if (len(r) != 0) != refused {
t.Errorf("%s on two nodes: refusals %v, want refused %v", name, r, refused) t.Errorf("%s on two nodes: refusals %v, want refused %v", name, r, refused)
} }
@@ -89,7 +89,7 @@ func TestPlacement(t *testing.T) {
twice := novox twice := novox
twice.Node = "stranger" twice.Node = "stranger"
twice.Decided.Address = "10.77.0.2" twice.Decided.Address = "10.77.0.2"
r := CheckPlacements(map[string]seat.Placement{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled) r := CheckAssignments(map[string]seat.Assignments{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled)
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080", for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record", "bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record",
"mesh-store, held once for the mesh, is held on novox and stranger", "mesh-store, held once for the mesh, is held on novox and stranger",
@@ -113,7 +113,7 @@ func TestPlacement(t *testing.T) {
t.Fatalf("laid %+v, %v", laid, findings) t.Fatalf("laid %+v, %v", laid, findings)
} }
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) { if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
t.Error("laying changed the current placement") t.Error("laying changed the current assignments")
} }
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" { 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) t.Errorf("a node with no key: %+v, %v", fresh, f)
@@ -125,7 +125,7 @@ func TestPlacement(t *testing.T) {
"not placed": {seat.ProposalChange{Op: client.OpTakeOff, Node: "g14", Module: "nope"}, "not placed"}, "not placed": {seat.ProposalChange{Op: client.OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
"no such field": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"}, "no such field": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
"wrong type": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"}, "wrong type": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of placement"}, "unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of assignments"},
"pin of nobody": {seat.ProposalChange{Op: client.OpPin, Node: "g14", Provision: "x"}, "provider"}, "pin of nobody": {seat.ProposalChange{Op: client.OpPin, Node: "g14", Provision: "x"}, "provider"},
"not held": {seat.ProposalChange{Op: client.OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"}, "not held": {seat.ProposalChange{Op: client.OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
"bad port": {seat.ProposalChange{Op: client.OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"}, "bad port": {seat.ProposalChange{Op: client.OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
@@ -190,8 +190,8 @@ func TestPlacement(t *testing.T) {
t.Errorf("the digest of %s is %s, and the earlier holder made %s", name, c.got, c.want) t.Errorf("the digest of %s is %s, and the earlier holder made %s", name, c.got, c.want)
} }
} }
if ExportDigest(current) != ExportDigest(map[string]seat.Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) { if ExportDigest(current) != ExportDigest(map[string]seat.Assignments{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
t.Error("the export digest depends on order or not on the placements") t.Error("the export digest depends on order or not on the assignments")
} }
// 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one // 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one
// change, from the enroller alone; a warrant naming another node or address is refused. // change, from the enroller alone; a warrant naming another node or address is refused.
@@ -267,7 +267,7 @@ func TestPlacement(t *testing.T) {
AddNodeRefusal(remove, enroller, enrollers) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) { AddNodeRefusal(remove, enroller, enrollers) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enrollers)) t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enrollers))
} }
joined := map[string]seat.Placement{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}} joined := map[string]seat.Assignments{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}}
if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 { if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 {
t.Errorf("a joining node removed: %v, %v", laid, f) t.Errorf("a joining node removed: %v, %v", laid, f)
} }
@@ -301,9 +301,9 @@ func TestPlacement(t *testing.T) {
} }
}) })
// ADR 0571 clause 4: who holds which seat, from placement; a node seat by its node. // ADR 0571 clause 4: who holds which seat, from assignments; a node seat by its node.
t.Run("who holds which seat", func(t *testing.T) { t.Run("who holds which seat", func(t *testing.T) {
held := SeatHoldersOf(map[string]seat.Placement{ held := SeatHoldersOf(map[string]seat.Assignments{
"ace": {Node: "ace", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}}, "ace": {Node: "ace", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}},
"novox": {Node: "novox", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "mesh-broker", Scope: "mesh", Module: "nats"}, "novox": {Node: "novox", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "mesh-broker", Scope: "mesh", Module: "nats"},
{Claim: "photo-gallery", Scope: "mesh", Module: "photos"}}}, {Claim: "photo-gallery", Scope: "mesh", Module: "photos"}}},
@@ -1,17 +1,17 @@
package placement package assignment
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
) )
// Placement candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key // Assignments candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key
// `<proposal>.<node>` in its state `candidates`: the node's placement with the proposal laid over it for a node whose key // `<proposal>.<node>` in its state `candidates`: the node's assignments with the proposal laid over it for a node whose key
// it changes, or the node's current key for one whose composition it changes only through another node (Reached). The // it changes, or the node's current key for one whose composition it changes only through another node (Reached). The
// layouts are the seat's contract's (CandidatesByProposalNodeValue); what refuses a candidate is here. // layouts are the seat's contract's (CandidatesByProposalNodeValue); what refuses a candidate is here.
@@ -31,7 +31,7 @@ func CandidateKey(proposal, node string) (string, error) {
func ParseCandidateKey(key string) (proposal, node string, err error) { func ParseCandidateKey(key string) (proposal, node string, err error) {
proposal, node, ok := strings.Cut(key, ".") proposal, node, ok := strings.Cut(key, ".")
if !ok || keys.CheckName("node", node) != nil || client.CheckProposalID(proposal) != nil { if !ok || keys.CheckName("node", node) != nil || client.CheckProposalID(proposal) != nil {
return "", "", fmt.Errorf("placement: %q is not a placement candidate key, <proposal>.<node>", key) return "", "", fmt.Errorf("assignments: %q is not an assignments candidate key, <proposal>.<node>", key)
} }
return proposal, node, nil return proposal, node, nil
} }
@@ -40,8 +40,8 @@ func checkCandidate(c seat.CandidatesByProposalNodeValue) error {
if err := client.CheckProposalID(c.Proposal); err != nil { if err := client.CheckProposalID(c.Proposal); err != nil {
return err return err
} }
if c.Placement != nil { if c.Assignments != nil {
return client.CheckPlacement(*c.Placement) return client.CheckAssignments(*c.Assignments)
} }
return nil return nil
} }
@@ -52,15 +52,15 @@ func EncodeCandidate(c seat.CandidatesByProposalNodeValue) ([]byte, error) {
return nil, err return nil, err
} }
c.V = keys.Version c.V = keys.Version
if c.Placement != nil { if c.Assignments != nil {
p := *c.Placement p := *c.Assignments
p.V = keys.Version p.V = keys.Version
c.Placement = &p c.Assignments = &p
} }
return json.Marshal(c) return json.Marshal(c)
} }
// DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and a placement the // DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and assignments the
// client refuses. // client refuses.
func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) { func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
if err := keys.CheckVersion(data); err != nil { if err := keys.CheckVersion(data); err != nil {
@@ -68,7 +68,7 @@ func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
} }
c, err := seat.DecodeCandidatesByProposalNodeValue(data) c, err := seat.DecodeCandidatesByProposalNodeValue(data)
if err != nil { if err != nil {
return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("placement: a candidate does not fit its shape: %w", err) return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("assignments: a candidate does not fit its shape: %w", err)
} }
if err := checkCandidate(c); err != nil { if err := checkCandidate(c); err != nil {
return seat.CandidatesByProposalNodeValue{}, err return seat.CandidatesByProposalNodeValue{}, err
@@ -1,9 +1,9 @@
// Package placement is the assigner's own check of a placement and of a proposal that changes it (novox/hq ADR 0567 §2, // Package assignments is the assigner's own check of assignments and of a proposal that changes it (novox/hq ADR 0567 §2,
// ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes // ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes
// anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the // anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the
// placement shares (the keys assembled into a node's placement, the node key's digest, who holds a seat) is the seat's // assignments shares (the keys assembled into a node's assignments, the node key's digest, who holds a seat) is the seat's
// client's (package meshmoduleassignerclient). Nothing here is read by another module. // client's (package meshmoduleassignerclient). Nothing here is read by another module.
package placement package assignment
import ( import (
"encoding/json" "encoding/json"
@@ -11,8 +11,8 @@ import (
"sort" "sort"
"strings" "strings"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
) )
@@ -25,19 +25,19 @@ var KeptApart = []struct{ A, B, Record string }{
{A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"}, {A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"},
} }
// CheckPlacements is the holder's check of a proposal laid over every placement (ADR 0567 §2), over the placements by // CheckAssignments is the holder's check of a proposal laid over every assignments (ADR 0567 §2), over the assignments by
// node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the // node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the
// first: a placement naming a node not enrolled or a module that is not known (held by the indexer, or provided by the // first: assignments naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
// mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding // mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding
// naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated // naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated
// (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private // (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private
// network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a // network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a
// requirement left unbound are the composition's to find (its ForeseenPorts and Unbound). // requirement left unbound are the composition's to find (its ForeseenPorts and Unbound).
func CheckPlacements(placements map[string]seat.Placement, known func(module string) bool, enrolled func(node string) bool) []string { func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string {
var refusals []string var refusals []string
nodes := sortedKeys(placements) nodes := sortedKeys(assignments)
placed := func(p seat.Provider) bool { placed := func(p seat.Provider) bool {
other, ok := placements[p.Node] other, ok := assignments[p.Node]
if !ok { if !ok {
return false return false
} }
@@ -52,9 +52,9 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
addresses := map[string]string{} addresses := map[string]string{}
var hubs []string var hubs []string
for _, node := range nodes { for _, node := range nodes {
p := placements[node] p := assignments[node]
if p.Node != node { if p.Node != node {
refusals = append(refusals, fmt.Sprintf("the placement of %s names the node %q", node, p.Node)) refusals = append(refusals, fmt.Sprintf("the assignments of %s names the node %q", node, p.Node))
} }
if a := strings.TrimSpace(p.Decided.Address); a != "" { if a := strings.TrimSpace(p.Decided.Address); a != "" {
if other, twice := addresses[a]; twice { if other, twice := addresses[a]; twice {
@@ -122,8 +122,8 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
return refusals return refusals
} }
// Change is what `placement-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had // Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
// none) and after (0: the key was deleted), and its modules added, removed or changed against the placement before. // none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before.
type Change struct { type Change struct {
Node string `json:"node"` Node string `json:"node"`
Before uint64 `json:"before"` Before uint64 `json:"before"`
@@ -141,10 +141,10 @@ func (c Change) Body() map[string]any {
return out return out
} }
// ChangeOf is what changed from before to after on one node; before is the zero Placement for a node new to placement. // ChangeOf is what changed from before to after on one node; before is the zero Assignments for a node new to assignments.
// A change of the node's pins or holdings counts as a change of every module placed on it both times, since the // A change of the node's pins or holdings counts as a change of every module placed on it both times, since the
// composition of each may read them. // composition of each may read them.
func ChangeOf(before, after seat.Placement) Change { func ChangeOf(before, after seat.Assignments) Change {
c := Change{Node: after.Node} c := Change{Node: after.Node}
if c.Node == "" { if c.Node == "" {
c.Node = before.Node c.Node = before.Node
@@ -176,7 +176,7 @@ func ChangeOf(before, after seat.Placement) Change {
return c return c
} }
// SortChanges orders a placement-changed event's nodes by node. // SortChanges orders assignments-changed event's nodes by node.
func SortChanges(c []Change) { func SortChanges(c []Change) {
sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node }) sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node })
} }
@@ -187,14 +187,14 @@ func jsonEqual(a, b any) bool {
return string(ja) == string(jb) return string(ja) == string(jb)
} }
// SeatHoldersOf is the state composed from every node's placement (ADR 0567 §1): each holding by the seat it claims, // SeatHoldersOf is the state composed from every node's assignments (ADR 0567 §1): each holding by the seat it claims,
// keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name // keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name
// is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes // is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes
// only a key that differs. // only a key that differs.
func SeatHoldersOf(placements map[string]seat.Placement) map[string]seat.SeatHolders { func SeatHoldersOf(assignments map[string]seat.Assignments) map[string]seat.SeatHolders {
out := map[string]seat.SeatHolders{} out := map[string]seat.SeatHolders{}
for _, n := range sortedKeys(placements) { for _, n := range sortedKeys(assignments) {
for _, h := range placements[n].Holds { for _, h := range assignments[n].Holds {
node := "" node := ""
if h.Scope == "node" { if h.Scope == "node" {
node = n node = n
@@ -1,4 +1,4 @@
package placement package assignment
import ( import (
"crypto/sha256" "crypto/sha256"
@@ -6,7 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
) )
@@ -72,7 +72,7 @@ type tunnelPeerForm struct {
Address string `json:"address,omitempty"` Address string `json:"address,omitempty"`
} }
type placementForm struct { type assignmentsForm struct {
V int `json:"v"` V int `json:"v"`
Node string `json:"node"` Node string `json:"node"`
Revision string `json:"revision,omitempty"` Revision string `json:"revision,omitempty"`
@@ -107,7 +107,7 @@ type changeForm struct {
Layer map[string]any `json:"layer,omitempty"` Layer map[string]any `json:"layer,omitempty"`
MeshValues map[string]any `json:"mesh-wide-values,omitempty"` MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
Values map[string]any `json:"values,omitempty"` Values map[string]any `json:"values,omitempty"`
Whole *placementForm `json:"whole,omitempty"` Whole *assignmentsForm `json:"whole,omitempty"`
Decided *decidedForm `json:"decided,omitempty"` Decided *decidedForm `json:"decided,omitempty"`
Keys *joiningForm `json:"keys,omitempty"` Keys *joiningForm `json:"keys,omitempty"`
} }
@@ -142,8 +142,8 @@ func decidedOf(d seat.NodeDecided) decidedForm {
Adopted: d.Adopted, Taken: d.Taken, Tunnel: tunnelOf(d.Tunnel), PeerNames: d.PeerNames} Adopted: d.Adopted, Taken: d.Taken, Tunnel: tunnelOf(d.Tunnel), PeerNames: d.PeerNames}
} }
func placementOf(p seat.Placement) placementForm { func assignmentsOf(p seat.Assignments) assignmentsForm {
out := placementForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided), out := assignmentsForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
Pins: providersOf(p.Pins)} Pins: providersOf(p.Pins)}
if p.Modules != nil { if p.Modules != nil {
out.Modules = []placedForm{} out.Modules = []placedForm{}
@@ -170,7 +170,7 @@ func changeOf(c seat.ProposalChange) changeForm {
out.Value, _ = json.Marshal(c.Value) out.Value, _ = json.Marshal(c.Value)
} }
if c.Whole != nil { if c.Whole != nil {
w := placementOf(*c.Whole) w := assignmentsOf(*c.Whole)
out.Whole = &w out.Whole = &w
} }
if c.Decided != nil { if c.Decided != nil {
@@ -183,14 +183,14 @@ func changeOf(c seat.ProposalChange) changeForm {
return out return out
} }
// ExportDigest is the digest of an export's answer, every node's placement, as the seed proposes it: the same // ExportDigest is the digest of an export's answer, every node's assignments, as the seed proposes it: the same
// placements give the same digest, whatever order they came in. // assignments give the same digest, whatever order they came in.
func ExportDigest(placements map[string]seat.Placement) string { func ExportDigest(assignments map[string]seat.Assignments) string {
h := sha256.New() h := sha256.New()
for _, node := range sortedKeys(placements) { for _, node := range sortedKeys(assignments) {
p := placements[node] p := assignments[node]
p.V = keys.Version // the export was made with the placement encoded at the keys' version p.V = keys.Version // the export was made with the assignments encoded at the keys' version
raw, _ := json.Marshal(placementOf(p)) raw, _ := json.Marshal(assignmentsOf(p))
fmt.Fprintf(h, "%s\n%s\n", node, raw) fmt.Fprintf(h, "%s\n%s\n", node, raw)
} }
return "sha256:" + hex.EncodeToString(h.Sum(nil)) return "sha256:" + hex.EncodeToString(h.Sum(nil))
@@ -1,4 +1,4 @@
package placement package assignment
import ( import (
"os" "os"
@@ -1,4 +1,4 @@
package placement package assignment
import ( import (
"crypto/rand" "crypto/rand"
@@ -9,13 +9,13 @@ import (
"strings" "strings"
"time" "time"
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
enroller "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" enroller "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
) )
// A placement changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder // Assignments changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder
// lays them over the current keys, names the revision of each key it laid them over, and either refuses at once with every // lays them over the current keys, names the revision of each key it laid them over, and either refuses at once with every
// finding or writes the proposal under its id in its state `proposals` and a candidate for each node it must hear from. // finding or writes the proposal under its id in its state `proposals` and a candidate for each node it must hear from.
@@ -50,7 +50,7 @@ func CheckChange(c seat.ProposalChange) error {
} }
needs := func(what string, ok bool) error { needs := func(what string, ok bool) error {
if !ok { if !ok {
return fmt.Errorf("placement: a %s change of %s names no %s", c.Op, c.Node, what) return fmt.Errorf("assignments: a %s change of %s names no %s", c.Op, c.Node, what)
} }
return nil return nil
} }
@@ -67,50 +67,50 @@ func CheckChange(c seat.ProposalChange) error {
return needs("field or value", c.Field != "" && c.Value != nil) return needs("field or value", c.Field != "" && c.Value != nil)
case client.OpSeed: case client.OpSeed:
if c.Whole == nil || c.Whole.Node != c.Node { if c.Whole == nil || c.Whole.Node != c.Node {
return fmt.Errorf("placement: the seed of %s names no placement of that node", c.Node) return fmt.Errorf("assignments: the seed of %s names no assignments of that node", c.Node)
} }
return client.CheckPlacement(*c.Whole) return client.CheckAssignments(*c.Whole)
case client.OpAddNode: case client.OpAddNode:
if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Overlay == "" { if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Overlay == "" {
return fmt.Errorf("placement: the add-node of %s names no address or no tunnel key", c.Node) return fmt.Errorf("assignments: the add-node of %s names no address or no tunnel key", c.Node)
} }
// The identity keys come at redeem, whole or not at all: an identity with no sealing key, or the reverse. // The identity keys come at redeem, whole or not at all: an identity with no sealing key, or the reverse.
if (c.Keys.Identity == "") != (c.Keys.Sealing == "") || (c.Keys.Identity == "" && c.Keys.Serving != "") { if (c.Keys.Identity == "") != (c.Keys.Sealing == "") || (c.Keys.Identity == "" && c.Keys.Serving != "") {
return fmt.Errorf("placement: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node) return fmt.Errorf("assignments: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node)
} }
if c.Decided.Hub { if c.Decided.Hub {
return fmt.Errorf("placement: the add-node of %s makes it the hub, which a joining node never is", c.Node) return fmt.Errorf("assignments: the add-node of %s makes it the hub, which a joining node never is", c.Node)
} }
if c.Module != "" || len(c.Ports)+len(c.Bindings) > 0 || c.Whole != nil { if c.Module != "" || len(c.Ports)+len(c.Bindings) > 0 || c.Whole != nil {
return fmt.Errorf("placement: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node) return fmt.Errorf("assignments: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
} }
return nil return nil
case client.OpRemoveNode: case client.OpRemoveNode:
if c.Module != "" || c.Decided != nil || c.Keys != nil || c.Whole != nil { if c.Module != "" || c.Decided != nil || c.Keys != nil || c.Whole != nil {
return fmt.Errorf("placement: the remove-node of %s names more than its node", c.Node) return fmt.Errorf("assignments: the remove-node of %s names more than its node", c.Node)
} }
return nil return nil
} }
return fmt.Errorf("placement: %q is no change of placement: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op) return fmt.Errorf("assignments: %q is no change of assignments: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op)
} }
// LayChanges is the placements of the nodes the changes name with the changes laid over current, in the order given; // LayChanges is the assignments of the nodes the changes name with the changes laid over current, in the order given;
// current is every node's placement now (absent: the node has no key). It returns, by node, the laid placement, and // current is every node's assignments now (absent: the node has no key). It returns, by node, the laid assignments, and
// every finding: a change that does not fit its node (a module taken off that is not placed, configured or holding // every finding: a change that does not fit its node (a module taken off that is not placed, configured or holding
// there; a field NodeDecided does not have) or a placement the client refuses. What CheckPlacements refuses across nodes // there; a field NodeDecided does not have) or assignments the client refuses. What CheckAssignments refuses across nodes
// is the caller's to ask of the result laid over current. // is the caller's to ask of the result laid over current.
func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange) (map[string]seat.Placement, []string) { func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChange) (map[string]seat.Assignments, []string) {
laid := map[string]seat.Placement{} laid := map[string]seat.Assignments{}
var findings []string var findings []string
of := func(node string) seat.Placement { of := func(node string) seat.Assignments {
if p, ok := laid[node]; ok { if p, ok := laid[node]; ok {
return p return p
} }
p, ok := current[node] p, ok := current[node]
if !ok { if !ok {
return seat.Placement{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}} return seat.Assignments{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}}
} }
return clonePlacement(p) return cloneAssignments(p)
} }
for i, c := range changes { for i, c := range changes {
if err := CheckChange(c); err != nil { if err := CheckChange(c); err != nil {
@@ -129,14 +129,14 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
} }
switch c.Op { switch c.Op {
case client.OpSeed: case client.OpSeed:
p = clonePlacement(*c.Whole) p = cloneAssignments(*c.Whole)
case client.OpAddNode: case client.OpAddNode:
// A joining node has no key yet (ADR 0587 §7): one that has is no joining node. // A joining node has no key yet (ADR 0587 §7): one that has is no joining node.
if _, has := current[c.Node]; has { if _, has := current[c.Node]; has {
findings = append(findings, fmt.Sprintf("change %d: %s is placed already, so it does not join", i+1, c.Node)) findings = append(findings, fmt.Sprintf("change %d: %s is placed already, so it does not join", i+1, c.Node))
continue continue
} }
p = seat.Placement{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}} p = seat.Assignments{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
case client.OpRemoveNode: case client.OpRemoveNode:
// Only a joining node never confirmed goes this way: one with no module and no holding (a node that runs // Only a joining node never confirmed goes this way: one with no module and no holding (a node that runs
// anything is taken off module by module, by a proposal the operator approves). // anything is taken off module by module, by a proposal the operator approves).
@@ -227,7 +227,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
laid[c.Node] = p laid[c.Node] = p
} }
for _, node := range sortedKeys(laid) { for _, node := range sortedKeys(laid) {
if err := client.CheckPlacement(laid[node]); err != nil { if err := client.CheckAssignments(laid[node]); err != nil {
findings = append(findings, err.Error()) findings = append(findings, err.Error())
} }
} }
@@ -275,9 +275,9 @@ var decidedFields = func() map[string]bool {
return out return out
}() }()
func clonePlacement(p seat.Placement) seat.Placement { func cloneAssignments(p seat.Assignments) seat.Assignments {
raw, _ := json.Marshal(p) raw, _ := json.Marshal(p)
var out seat.Placement var out seat.Assignments
_ = json.Unmarshal(raw, &out) _ = json.Unmarshal(raw, &out)
if out.Modules == nil { if out.Modules == nil {
out.Modules = []seat.PlacedModule{} out.Modules = []seat.PlacedModule{}
@@ -301,33 +301,33 @@ func CheckProposal(p seat.Proposal) error {
return err return err
} }
if strings.TrimSpace(p.By) == "" || strings.TrimSpace(p.Why) == "" { if strings.TrimSpace(p.By) == "" || strings.TrimSpace(p.Why) == "" {
return fmt.Errorf("placement: the proposal %s names no proposer or no why", p.ID) return fmt.Errorf("assignments: the proposal %s names no proposer or no why", p.ID)
} }
if len(p.Base) == 0 || (len(p.Changes) == 0) != p.HandBack || (p.HandBack && p.Seed) { if len(p.Base) == 0 || (len(p.Changes) == 0) != p.HandBack || (p.HandBack && p.Seed) {
return fmt.Errorf("placement: the proposal %s changes nothing, or is a hand-back with changes", p.ID) return fmt.Errorf("assignments: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
} }
for i, c := range p.Changes { for i, c := range p.Changes {
if err := CheckChange(c); err != nil { if err := CheckChange(c); err != nil {
return fmt.Errorf("placement: the proposal %s, change %d: %w", p.ID, i+1, err) return fmt.Errorf("assignments: the proposal %s, change %d: %w", p.ID, i+1, err)
} }
if (c.Op == client.OpSeed) != p.Seed { if (c.Op == client.OpSeed) != p.Seed {
return fmt.Errorf("placement: the proposal %s mixes the seed with other changes", p.ID) return fmt.Errorf("assignments: the proposal %s mixes the seed with other changes", p.ID)
} }
if _, ok := p.Base[c.Node]; !ok { if _, ok := p.Base[c.Node]; !ok {
return fmt.Errorf("placement: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node) return fmt.Errorf("assignments: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node)
} }
} }
switch p.State { switch p.State {
case client.ProposalAnswering, client.ProposalFailed: case client.ProposalAnswering, client.ProposalFailed:
case client.ProposalAsked: case client.ProposalAsked:
if p.Ask == "" || p.Verdict == nil || p.Verdict.State != VerdictPass { if p.Ask == "" || p.Verdict == nil || p.Verdict.State != VerdictPass {
return fmt.Errorf("placement: the proposal %s is asked with no ask or no passing verdict", p.ID) return fmt.Errorf("assignments: the proposal %s is asked with no ask or no passing verdict", p.ID)
} }
default: default:
return fmt.Errorf("placement: the proposal %s is in no state %q", p.ID, p.State) return fmt.Errorf("assignments: the proposal %s is in no state %q", p.ID, p.State)
} }
if p.Seed && p.ExportDigest == "" { if p.Seed && p.ExportDigest == "" {
return fmt.Errorf("placement: the seed %s names no export digest", p.ID) return fmt.Errorf("assignments: the seed %s names no export digest", p.ID)
} }
return checkAddNode(p) return checkAddNode(p)
} }
@@ -344,25 +344,25 @@ func checkAddNode(p seat.Proposal) error {
} }
if adds == 0 { if adds == 0 {
if p.Warrant != nil { if p.Warrant != nil {
return fmt.Errorf("placement: the proposal %s carries an enrolment's warrant and adds no node", p.ID) return fmt.Errorf("assignments: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
} }
return nil return nil
} }
if adds != 1 || len(p.Changes) != 1 || p.HandBack || p.Seed { if adds != 1 || len(p.Changes) != 1 || p.HandBack || p.Seed {
return fmt.Errorf("placement: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID) return fmt.Errorf("assignments: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
} }
c, w := p.Changes[0], p.Warrant c, w := p.Changes[0], p.Warrant
if w == nil || w.Ask == "" || w.Warrant == "" { if w == nil || w.Ask == "" || w.Warrant == "" {
return fmt.Errorf("placement: the add-node of %s carries no warrant of its enrolment", c.Node) return fmt.Errorf("assignments: the add-node of %s carries no warrant of its enrolment", c.Node)
} }
if c.Op == client.OpRemoveNode { if c.Op == client.OpRemoveNode {
if w.Node != c.Node { if w.Node != c.Node {
return fmt.Errorf("placement: the remove-node of %s carries the warrant of %s", c.Node, w.Node) return fmt.Errorf("assignments: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
} }
return nil return nil
} }
if w.Node != c.Node || w.Address != c.Decided.Address { if w.Node != c.Node || w.Address != c.Decided.Address {
return fmt.Errorf("placement: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address) return fmt.Errorf("assignments: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address)
} }
return nil return nil
} }
@@ -383,10 +383,10 @@ func AddNodeRefusal(p seat.Proposal, caller string, enrollers []string) string {
return "" return ""
} }
// EnrollersOf is the modules that hold the seat mesh-node-enroller on the placements given, sorted: the state seat-holders // EnrollersOf is the modules that hold the seat mesh-node-enroller on the assignments given, sorted: the state seat-holders
// composed from them (SeatHoldersOf), read by the client's HoldersOf. // composed from them (SeatHoldersOf), read by the client's HoldersOf.
func EnrollersOf(placements map[string]seat.Placement) []string { func EnrollersOf(assignments map[string]seat.Assignments) []string {
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(placements)), enroller.Seat, "") holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(assignments)), enroller.Seat, "")
var out []string var out []string
for _, h := range holders { for _, h := range holders {
if !slices.Contains(out, h.Module) { if !slices.Contains(out, h.Module) {
@@ -422,7 +422,7 @@ func DecodeProposal(data []byte) (seat.Proposal, error) {
} }
p, err := seat.DecodeProposalsByProposalValue(data) p, err := seat.DecodeProposalsByProposalValue(data)
if err != nil { if err != nil {
return seat.Proposal{}, fmt.Errorf("placement: a proposal does not fit its shape: %w", err) return seat.Proposal{}, fmt.Errorf("assignments: a proposal does not fit its shape: %w", err)
} }
if err := CheckProposal(p); err != nil { if err := CheckProposal(p); err != nil {
return seat.Proposal{}, err return seat.Proposal{}, err
@@ -1,6 +1,6 @@
package placement package assignment
import seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" import seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
// Module is the module that holds the seat mesh-module-assigner: the states of the seat are named after it, as every // Module is the module that holds the seat mesh-module-assigner: the states of the seat are named after it, as every
// module state is (a bucket is `<module>_<state>`), and its manifest declares them. // module state is (a bucket is `<module>_<state>`), and its manifest declares them.
@@ -8,7 +8,7 @@ const Module = "mesh-delivery-assigner"
// The buckets of the seat's states, which only the holder writes (ADR 0567 §1). // The buckets of the seat's states, which only the holder writes (ADR 0567 §1).
const ( const (
PlacementBucket = Module + "_" + seat.StatePlacement AssignmentsBucket = Module + "_" + seat.StateAssignments
ProposalsBucket = Module + "_" + seat.StateProposals ProposalsBucket = Module + "_" + seat.StateProposals
CandidatesBucket = Module + "_" + seat.StateCandidates CandidatesBucket = Module + "_" + seat.StateCandidates
PulsesBucket = Module + "_" + seat.StatePulses PulsesBucket = Module + "_" + seat.StatePulses
@@ -16,8 +16,8 @@ const (
) )
const ( const (
// PlacementHistory is the revisions the placement bucket keeps per key: the bus's most (§6). // AssignmentsHistory is the revisions the assignments bucket keeps per key: the bus's most (§6).
PlacementHistory = 64 AssignmentsHistory = 64
// EventDays is how long the seat's events are kept: they ride the shared events stream on the seat's own subject // EventDays is how long the seat's events are kept: they ride the shared events stream on the seat's own subject
// (the seat's family, 7 days). // (the seat's family, 7 days).
EventDays = 7 EventDays = 7
@@ -7,13 +7,13 @@ package buckets
import ( import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// The states the assigner keeps: the wants seat's, and its own asks, puts and policies. // The states the assigner keeps: the wants seat's, and its own asks, puts and policies.
@@ -1,5 +1,5 @@
// Package judge is how the seat mesh-module-assigner's holder judges a proposal (novox/hq ADR 0567 §3, §4, as ADR 0593 §8 // Package judge is how the seat mesh-module-assigner's holder judges a proposal (novox/hq ADR 0567 §3, §4, as ADR 0593 §8
// changes them): what it changes on each node by its placement (Intended), every node answering it, and the verdict of // changes them): what it changes on each node by its assignments (Intended), every node answering it, and the verdict of
// their answers (JudgeAnswers). The assigner's own (ADR 0593 §1), moved back from mesh-sdk's compose/judge: the holder // their answers (JudgeAnswers). The assigner's own (ADR 0593 §1), moved back from mesh-sdk's compose/judge: the holder
// composes nothing, since every composition is its node's engine's, and a node answers through the candidates path // composes nothing, since every composition is its node's engine's, and a node answers through the candidates path
// with what it composed. It holds no bus client and no timer: the holder gives it what it read. // with what it composed. It holds no bus client and no timer: the holder gives it what it read.
@@ -17,24 +17,24 @@ import (
"time" "time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// AnswerBound is how long the holder waits for a node's answer after its candidate was written: the time to live of the // AnswerBound is how long the holder waits for a node's answer after its candidate was written: the time to live of the
// node's pulse key (placement.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer // node's pulse key (assignment.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer
// bound a check gives a node (ADR 0593 §8, §7: 60 seconds, nec.AnswerBound). A variable so a test on the bus // bound a check gives a node (ADR 0593 §8, §7: 60 seconds, nec.AnswerBound). A variable so a test on the bus
// can shorten it. // can shorten it.
var AnswerBound = nec.AnswerBound var AnswerBound = nec.AnswerBound
// Intended is, by node, the modules whose placed entry the proposal changes there, and the nodes that answer it: every // Intended is, by node, the modules whose placed entry the proposal changes there, and the nodes that answer it: every
// node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the placement as it // node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the assignments as it
// stands, laid the proposal's placements by node (nil removes the node's key) and nodes every node with a record. A // stands, laid the proposal's assignments by node (nil removes the node's key) and nodes every node with a record. A
// node's set holds the modules the proposal adds, removes or whose placed entry differs on that node; a node with no // node's set holds the modules the proposal adds, removes or whose placed entry differs on that node; a node with no
// key now gets an empty set (§7: the seed is answered as the same). What a node's compositions change beyond its own // key now gets an empty set (§7: the seed is answered as the same). What a node's compositions change beyond its own
// placement it says itself in its answer, marked in the brief as not computed. // assignments it says itself in its answer, marked in the brief as not computed.
func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement, nodes []string) (intended map[string]map[string]bool, must []string) { func Intended(current map[string]mma.Assignments, laid map[string]*mma.Assignments, nodes []string) (intended map[string]map[string]bool, must []string) {
intended = map[string]map[string]bool{} intended = map[string]map[string]bool{}
all := map[string]bool{} all := map[string]bool{}
for _, n := range nodes { for _, n := range nodes {
@@ -50,7 +50,7 @@ func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement,
before, placed := current[node] before, placed := current[node]
after, changesKey := laid[node] after, changesKey := laid[node]
if !changesKey || !placed { if !changesKey || !placed {
continue // untouched, or new to placement: every answer must be the same continue // untouched, or new to assignments: every answer must be the same
} }
var afterModules []mma.PlacedModule var afterModules []mma.PlacedModule
if after != nil { if after != nil {
@@ -8,20 +8,20 @@ import (
"testing" "testing"
"time" "time"
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
plc "mesh-delivery-assigner/internal/placement" plc "mesh-delivery-assigner/internal/assignment"
) )
// ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's placement, ADR 0559 §5): what a proposal // ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's assignments, ADR 0559 §5): what a proposal
// changes by node, and the verdict of the answers: differences shown and failing nothing, a refusal and a silent node // changes by node, and the verdict of the answers: differences shown and failing nothing, a refusal and a silent node
// failing, a later answer judging again, the seed failing on any difference. // failing, a later answer judging again, the seed failing on any difference.
func TestJudge(t *testing.T) { func TestJudge(t *testing.T) {
current := map[string]mma.Placement{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}} current := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
laid := map[string]*mma.Placement{ laid := map[string]*mma.Assignments{
"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail", Ports: map[string]int64{"25": 2525}}, {Module: "web"}}}, "anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail", Ports: map[string]int64{"25": 2525}}, {Module: "web"}}},
"laptop": {Node: "laptop", Modules: []mma.PlacedModule{{Module: "web"}}}} "laptop": {Node: "laptop", Modules: []mma.PlacedModule{{Module: "web"}}}}
// ADR 0593 §8: every node answers; what the placement changes is read from the placement alone. // ADR 0593 §8: every node answers; what the assignments changes is read from the assignments alone.
got, must := Intended(current, laid, []string{"anchor", "laptop", "server"}) got, must := Intended(current, laid, []string{"anchor", "laptop", "server"})
if !got["anchor"]["mail"] || !got["anchor"]["web"] || len(got["laptop"]) != 0 || len(got["server"]) != 0 || if !got["anchor"]["mail"] || !got["anchor"]["web"] || len(got["laptop"]) != 0 || len(got["server"]) != 0 ||
!slices.Equal(must, []string{"anchor", "laptop", "server"}) { !slices.Equal(must, []string{"anchor", "laptop", "server"}) {
+11 -11
View File
@@ -1,8 +1,8 @@
package manifest package manifest
import ( import (
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"os" "os"
"reflect" "reflect"
"slices" "slices"
@@ -63,7 +63,7 @@ func TestManifest(t *testing.T) {
states[s.Name] = s.History states[s.Name] = s.History
} }
if want := map[string]int{"asks": 5, "differences": 64, "puts": 20, "wants": 20, if want := map[string]int{"asks": 5, "differences": 64, "puts": 20, "wants": 20,
"placement": 64, "proposals": 5, "candidates": 5, "pulses": 0, "findings": 5, "policies": 20, "seat-holders": 5, views.MarksState: 1, views.StatusState: 1, "assignments": 64, "placement": 64, "proposals": 5, "candidates": 5, "pulses": 0, "findings": 5, "policies": 20, "seat-holders": 5, views.MarksState: 1, views.StatusState: 1,
"engine-reads": 1}; !reflect.DeepEqual(states, want) { "engine-reads": 1}; !reflect.DeepEqual(states, want) {
t.Errorf("state = %v, want %v", states, want) t.Errorf("state = %v, want %v", states, want)
} }
@@ -81,20 +81,20 @@ func TestManifest(t *testing.T) {
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the // ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
// module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder. // module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder.
// ADR 0567 §1, §6-§10: it holds the seat mesh-module-assigner, serving its five verbs (adopt and hand-back went with the // ADR 0567 §1, §6-§10: it holds the seat mesh-module-assigner, serving its five verbs (adopt and hand-back went with the
// controller, ADR 0590); keeps the seat's events on its stream placement for 30 days; calls the hand-act log; and reads // controller, ADR 0590); keeps the seat's events on its stream assignments for 30 days; calls the hand-act log; and reads
// mesh-config's resolved values to compose every node a proposal touches. // mesh-config's resolved values to compose every node a proposal touches.
// ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus. // ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus.
if len(m.Claims) != 2 || m.Claims[0].Name != meshmoduleassigner.Seat || m.Claims[0].At() != catalogue.ScopeMesh || if len(m.Claims) != 2 || m.Claims[0].Name != meshmoduleassigner.Seat || m.Claims[0].At() != catalogue.ScopeMesh ||
!slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again"}) || !slices.Equal(m.Claims[0].Serves, []string{"assignments", "proposals", "propose", "withdraw", "again"}) ||
m.Claims[1].Name != meshwants.Seat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus"}) { m.Claims[1].Name != meshwants.Seat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus"}) {
t.Errorf("the assigner claims %+v, want mesh-module-assigner with its five verbs and mesh-wants with its two", m.Claims) t.Errorf("the assigner claims %+v, want mesh-module-assigner with its five verbs and mesh-wants with its two", m.Claims)
} }
// Whether the seats' definitions let it hold them is the manifest validator's (the judge, at the seat list). // Whether the seats' definitions let it hold them is the manifest validator's (the judge, at the seat list).
// ADR 0621 §1: the first-view tools placement, seat-holders and wants and design 25 § 12.2's six read tools, of the // ADR 0621 §1: the first-view tools assignments, seat-holders and wants and design 25 § 12.2's six read tools, of the
// program's bundle, which runs in a runtime of its own on the assigner's account; ADR 0618 §3: and assigner_engine_reads, // program's bundle, which runs in a runtime of its own on the assigner's account; ADR 0618 §3: and assigner_engine_reads,
// which lists and asks again what each node's engine reads. // which lists and asks again what each node's engine reads.
if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "placement", "seat-holders", "wants", if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assignments", "seat-holders", "wants",
"assigner_read_placement", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences", "assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences",
"assigner_first_views", "assigner_first_view_again", "assigner_engine_reads"}) { "assigner_first_views", "assigner_first_view_again", "assigner_engine_reads"}) {
t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools and assigner_engine_reads", m.Tools) t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools and assigner_engine_reads", m.Tools)
} }
@@ -102,7 +102,7 @@ func TestManifest(t *testing.T) {
t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs) t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs)
} }
// ADR 0621 §2: each first-view tool answers exactly its readers, never the assigner. // ADR 0621 §2: each first-view tool answers exactly its readers, never the assigner.
callers := map[string]string{"placement": "mesh-grants,node-tools,mesh-delivery-indexer", "seat-holders": "node-tools", "wants": "mesh-grants"} callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer", "seat-holders": "node-tools", "wants": "mesh-grants"}
env := m.Build.Artifacts[0].Env env := m.Build.Artifacts[0].Env
for tool, list := range callers { for tool, list := range callers {
if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) { if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) {
@@ -130,7 +130,7 @@ func TestManifest(t *testing.T) {
"seat:node-engine.manifest-reads"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { // ADR 0618 §3 "seat:node-engine.manifest-reads"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { // ADR 0618 §3
t.Errorf("invokes = %v, want %v", m.Invokes, want) t.Errorf("invokes = %v, want %v", m.Invokes, want)
} }
if want := []string{"put", "views.asks", "views.differences", "views.findings", "views.placement", "views.seat-holders", "views.wants"}; !slices.Equal(m.Emits, want) { if want := []string{"assignments-copied", "put", "views.asks", "views.differences", "views.findings", "views.assignments", "views.seat-holders", "views.wants"}; !slices.Equal(m.Emits, want) {
t.Errorf("emits = %v, want put and its six views", m.Emits) t.Errorf("emits = %v, want the copy, put and its six views", m.Emits)
} }
} }
+7 -5
View File
@@ -8,7 +8,7 @@
"name": "mesh-module-assigner", "name": "mesh-module-assigner",
"scope": "mesh", "scope": "mesh",
"serves": [ "serves": [
"placement", "assignments",
"proposals", "proposals",
"propose", "propose",
"withdraw", "withdraw",
@@ -56,11 +56,12 @@
"seat:node-engine.manifest-reads" "seat:node-engine.manifest-reads"
], ],
"emits": [ "emits": [
"assignments-copied",
"put", "put",
"views.asks", "views.asks",
"views.differences", "views.differences",
"views.findings", "views.findings",
"views.placement", "views.assignments",
"views.seat-holders", "views.seat-holders",
"views.wants" "views.wants"
], ],
@@ -68,10 +69,10 @@
"assigner_put", "assigner_put",
"assigner_release", "assigner_release",
"assigner_puts", "assigner_puts",
"placement", "assignments",
"seat-holders", "seat-holders",
"wants", "wants",
"assigner_read_placement", "assigner_read_assignments",
"assigner_read_seat_holders", "assigner_read_seat_holders",
"assigner_read_wants", "assigner_read_wants",
"assigner_read_findings", "assigner_read_findings",
@@ -82,6 +83,7 @@
"assigner_engine_reads" "assigner_engine_reads"
], ],
"state": [ "state": [
{ "name": "assignments", "history": 64 },
{ "name": "placement", "history": 64 }, { "name": "placement", "history": 64 },
{ "name": "proposals", "history": 5 }, { "name": "proposals", "history": 5 },
{ "name": "candidates", "history": 5 }, { "name": "candidates", "history": 5 },
@@ -147,7 +149,7 @@
"mesh-delivery-assigner" "mesh-delivery-assigner"
], ],
"env": { "env": {
"MESH_VIEW_CALLERS_PLACEMENT": "mesh-grants,node-tools,mesh-delivery-indexer", "MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer",
"MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools", "MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools",
"MESH_VIEW_CALLERS_WANTS": "mesh-grants" "MESH_VIEW_CALLERS_WANTS": "mesh-grants"
} }
@@ -11,7 +11,7 @@ import (
// The seat the assigner holds is mesh-module-assigner (novox/hq ADR 0634 §9): no message, tool text or comment of the module // The seat the assigner holds is mesh-module-assigner (novox/hq ADR 0634 §9): no message, tool text or comment of the module
// says the name it had before. // says the name it had before.
func TestNoTextSaysTheOldSeatName(t *testing.T) { func TestNoTextSaysTheOldSeatName(t *testing.T) {
old := "mesh-" + "placement" old := "mesh-" + "assignments"
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || path == "oldseat_test.go" { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || path == "oldseat_test.go" {
return err return err
@@ -5,8 +5,8 @@ go 1.26.0
require ( require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
@@ -4,9 +4,9 @@ package buildrun
// builder checks them against it) and from the build seats' states: a bucket is `<holder module>_<state>`, and no contract // builder checks them against it) and from the build seats' states: a bucket is `<holder module>_<state>`, and no contract
// names a holder (novox/hq ADR 0625 §4, ADR 0634 §1). // names a holder (novox/hq ADR 0625 §4, ADR 0634 §1).
const ( const (
// PlacementBucket is the assigner's state placement, which the toolchain gate reads for the placing of a build module. // AssignmentsBucket is the assigner's state assignments, which the toolchain gate reads for the placing of a build module.
AssignerModule = "mesh-delivery-assigner" AssignerModule = "mesh-delivery-assigner"
PlacementBucket = AssignerModule + "_placement" AssignmentsBucket = AssignerModule + "_assignments"
) )
// StepsBucket is the bucket of a build module's state steps. // StepsBucket is the bucket of a build module's state steps.
@@ -12,7 +12,7 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
) )
@@ -23,12 +23,12 @@ import (
// ace.typescript at v26.12.0, and every Node fingerprint said "builders disagree"). // ace.typescript at v26.12.0, and every Node fingerprint said "builders disagree").
// //
// ToolchainGate stands between the toolchains buckets and what takes their entries in (the indexer's PutToolchain, a // ToolchainGate stands between the toolchains buckets and what takes their entries in (the indexer's PutToolchain, a
// ToolchainSet): an entry is passed on only while the placement (the seat mesh-placement's state `placement`, held by // ToolchainSet): an entry is passed on only while the assignments (the seat mesh-module-assigner's state `assignments`, held by
// the assigner, PlacementBucket, read by meshmoduleassignerclient.Placed; ADR 0567, ADR 0549 §3) places its build module on // the assigner, AssignmentsBucket, read by meshmoduleassignerclient.Placed; ADR 0567, ADR 0549 §3) places its build module on
// its node, and only once it was written since that placement; it is passed on as deleted otherwise, said once, and // its node, and only once it was written since that assignments; it is passed on as deleted otherwise, said once, and
// passed on when its node writes it again. A report written before the placement is one an earlier run of the module // passed on when its node writes it again. A report written before the assignments is one an earlier run of the module
// left (issue 1940, 2026-10-25 20:56: mesh-build-node put on ace again brought ace.typescript v26.12.0 of 2026-10-23 // left (issue 1940, 2026-10-25 20:56: mesh-build-node put on ace again brought ace.typescript v26.12.0 of 2026-10-23
// back into the agreement before the module ran there). Until the placement is settled, and while no node's placement // back into the agreement before the module ran there). Until the assignments is settled, and while no node's assignments
// is held (before the assigner's adopt writes the first one, or where the state cannot be read), every entry is passed // is held (before the assigner's adopt writes the first one, or where the state cannot be read), every entry is passed
// on. Builders that run and report different versions still disagree: that refusal is the guard of novox/hq ADR 0465, // on. Builders that run and report different versions still disagree: that refusal is the guard of novox/hq ADR 0465,
// and it stays. // and it stays.
@@ -41,11 +41,11 @@ type ToolchainGate struct {
forward func(module, key string, value []byte, deleted bool) bool // answers whether it changed what it holds forward func(module, key string, value []byte, deleted bool) bool // answers whether it changed what it holds
logf func(format string, a ...any) logf func(format string, a ...any)
placed *meshmoduleassignerclient.Placed placed *meshmoduleassignerclient.Placed
known bool // the placement is settled known bool // the assignments is settled
raw map[string]map[string][]byte // module -> toolchains key -> value, as the bucket holds it raw map[string]map[string][]byte // module -> toolchains key -> value, as the bucket holds it
left map[string]string // <module>/<key> -> why it was said left out, so each reason is said once left map[string]string // <module>/<key> -> why it was said left out, so each reason is said once
// OnPlaced is told, outside the gate's lock, once the placement's history is read and after each revision of it // OnPlaced is told, outside the gate's lock, once the assignments's history is read and after each revision of it
// FollowPlacement hears from then on (the counted nodes, Nodes, may have changed). Nil tells nobody. // FollowAssignments hears from then on (the counted nodes, Nodes, may have changed). Nil tells nobody.
OnPlaced func() OnPlaced func()
} }
@@ -75,28 +75,28 @@ func (g *ToolchainGate) Toolchain(module, key string, value []byte, deleted bool
return g.pass(module, key) return g.pass(module, key)
} }
// Placement takes one revision of the seat mesh-placement's placement in (at is when the bus wrote it) and answers whether what it // Assignments takes one revision of the seat mesh-module-assigner's assignments in (at is when the bus wrote it) and answers whether what it
// passed on changed. A value that does not read is said, and places nothing on its node. // passed on changed. A value that does not read is said, and places nothing on its node.
func (g *ToolchainGate) Placement(key string, value []byte, deleted bool, at time.Time) bool { func (g *ToolchainGate) Assignments(key string, value []byte, deleted bool, at time.Time) bool {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
was := g.placed.Seeded() was := g.placed.Seeded()
node, changed, err := g.placed.Put(key, value, deleted, at) node, changed, err := g.placed.Put(key, value, deleted, at)
if err != nil { if err != nil {
g.logf("%s: %v (issue 1940)", PlacementBucket, err) g.logf("%s: %v (issue 1940)", AssignmentsBucket, err)
} }
if !g.known { if !g.known {
return false return false
} }
if was != g.placed.Seeded() { if was != g.placed.Seeded() {
// The first placement came, or the last went: every report's standing changed. // The first assignments came, or the last went: every report's standing changed.
return g.all() return g.all()
} }
return g.again(node, changed) return g.again(node, changed)
} }
// Nodes are the nodes the placement places any module on, sorted: the nodes the mesh counts (a node leaves the count // Nodes are the nodes the assignments places any module on, sorted: the nodes the mesh counts (a node leaves the count
// when the placement no longer names it; ADR 0624 §1). None until the placement is read. // when the assignments no longer names it; ADR 0624 §1). None until the assignments is read.
func (g *ToolchainGate) Nodes() []string { func (g *ToolchainGate) Nodes() []string {
var out []string var out []string
for node, modules := range g.placed.ByNode() { for node, modules := range g.placed.ByNode() {
@@ -108,8 +108,8 @@ func (g *ToolchainGate) Nodes() []string {
return out return out
} }
// Settled says the placement's history is read: from now on only the reports of placed nodes pass, once a node's // Settled says the assignments's history is read: from now on only the reports of placed nodes pass, once a node's
// placement is held. It answers whether what it passed on changed. // assignments is held. It answers whether what it passed on changed.
func (g *ToolchainGate) Settled() bool { func (g *ToolchainGate) Settled() bool {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
@@ -144,7 +144,7 @@ func (g *ToolchainGate) again(node string, modules []string) bool {
return changed return changed
} }
// pass hands one key on as it stands: its value when its node is placed and wrote it since (or the placement is not // pass hands one key on as it stands: its value when its node is placed and wrote it since (or the assignments is not
// known), deleted otherwise. The caller holds mu. // known), deleted otherwise. The caller holds mu.
func (g *ToolchainGate) pass(module, key string) bool { func (g *ToolchainGate) pass(module, key string) bool {
value, held := g.raw[module][key] value, held := g.raw[module][key]
@@ -183,24 +183,24 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string {
return "" return ""
} }
// FollowPlacement reads the seat mesh-placement's placement with its history into gate, says it settled, and follows it; changed // FollowAssignments reads the seat mesh-module-assigner's assignments with its history into gate, says it settled, and follows it; changed
// is told whenever what the gate passes on changed. Where the state cannot be read or watched (not there, or this part // is told whenever what the gate passes on changed. Where the state cannot be read or watched (not there, or this part
// may not read it) it says so and the gate passes every report on: not fatal, since an indexer, builder or checker that // may not read it) it says so and the gate passes every report on: not fatal, since an indexer, builder or checker that
// stops for it stops every delivery (issue 1945). An error of the watch after that ends on the channel. // stops for it stops every delivery (issue 1945). An error of the watch after that ends on the channel.
func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *ToolchainGate, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) { func FollowAssignments(ctx context.Context, js process.BucketOpener, gate *ToolchainGate, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
errs := make(chan error, 1) errs := make(chan error, 1)
kv, err := js.KeyValue(ctx, PlacementBucket) kv, err := js.KeyValue(ctx, AssignmentsBucket)
if err != nil { if err != nil {
logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err) logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", AssignmentsBucket, err)
return errs, nil return errs, nil
} }
w, err := kv.WatchAll(ctx, jetstream.IncludeHistory()) w, err := kv.WatchAll(ctx, jetstream.IncludeHistory())
if err != nil { if err != nil {
logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err) logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", AssignmentsBucket, err)
return errs, nil return errs, nil
} }
put := func(e jetstream.KeyValueEntry) bool { put := func(e jetstream.KeyValueEntry) bool {
return gate.Placement(e.Key(), e.Value(), e.Operation() != jetstream.KeyValuePut, e.Created()) return gate.Assignments(e.Key(), e.Value(), e.Operation() != jetstream.KeyValuePut, e.Created())
} }
for settled := false; !settled; { for settled := false; !settled; {
select { select {
@@ -209,7 +209,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha
return errs, ctx.Err() return errs, ctx.Err()
case e, ok := <-w.Updates(): case e, ok := <-w.Updates():
if !ok { if !ok {
return errs, errors.New("the watch of " + PlacementBucket + " ended before it settled") return errs, errors.New("the watch of " + AssignmentsBucket + " ended before it settled")
} }
if e == nil { if e == nil {
settled = true settled = true
@@ -233,7 +233,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha
return return
case e, ok := <-w.Updates(): case e, ok := <-w.Updates():
if !ok { if !ok {
errs <- errors.New("the watch of " + PlacementBucket + " ended") errs <- errors.New("the watch of " + AssignmentsBucket + " ended")
return return
} }
if e != nil && put(e) { if e != nil && put(e) {
@@ -12,8 +12,8 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
) )
@@ -27,14 +27,14 @@ func gateReport(t *testing.T, node, version string, at time.Time) (string, []byt
return key, value return key, value
} }
// placing is a value of the state `placement` placing modules on node. // placing is a value of the state `assignments` placing modules on node.
func placing(t *testing.T, node string, modules ...string) (string, []byte) { func placing(t *testing.T, node string, modules ...string) (string, []byte) {
t.Helper() t.Helper()
p := meshmoduleassigner.Placement{Node: node} p := meshmoduleassigner.Assignments{Node: node}
for _, m := range modules { for _, m := range modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m}) p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
} }
values, err := meshmoduleassignerclient.PlacementValues(p) values, err := meshmoduleassignerclient.AssignmentsValues(p)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -42,29 +42,14 @@ func placing(t *testing.T, node string, modules ...string) (string, []byte) {
return key, values[key] return key, values[key]
} }
// placingLegacy is placing in the form before, `placement.<node>` (research 072 row 37).
func placingLegacy(t *testing.T, node string, modules ...string) (string, []byte) {
t.Helper()
p := meshmoduleassigner.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
key, _ := meshmoduleassignerclient.LegacyKey(node)
value, err := meshmoduleassignerclient.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
return key, value
}
func agreedOf(set *ToolchainSet) (string, string) { func agreedOf(set *ToolchainSet) (string, string) {
tc, none := buildstep.AgreedToolchain(set.Entries("typescript")) tc, none := buildstep.AgreedToolchain(set.Entries("typescript"))
return tc.Version, none return tc.Version, none
} }
// Issue 1940, the gate over the catalogue's placement (issue 774, ADR 0549 §3): a build node's report counts only while // Issue 1940, the gate over the catalogue's assignments (issue 774, ADR 0549 §3): a build node's report counts only while
// the placement places its build module on its node, and only once written since; before the placement is settled, and // the assignments places its build module on its node, and only once written since; before the assignments is settled, and
// while no node's placement is held, every report counts. The live cases: mesh-build-node taken off ace left // while no node's assignments is held, every report counts. The live cases: mesh-build-node taken off ace left
// ace.typescript at v26.12.0 against v26.12.1 on the three others; put on ace again, its report of 2026-10-23 06:29 came // ace.typescript at v26.12.0 against v26.12.1 on the three others; put on ace again, its report of 2026-10-23 06:29 came
// back before the module ran there. // back before the module ran there.
func TestToolchainGate(t *testing.T) { func TestToolchainGate(t *testing.T) {
@@ -78,7 +63,7 @@ func TestToolchainGate(t *testing.T) {
for _, node := range three { for _, node := range three {
if seeded { if seeded {
key, value := placing(t, node, NodeBuildModule) key, value := placing(t, node, NodeBuildModule)
gate.Placement(key, value, false, old.Add(-time.Hour)) gate.Assignments(key, value, false, old.Add(-time.Hour))
} }
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute)) key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
gate.Toolchain(NodeBuildModule, key, value, false) gate.Toolchain(NodeBuildModule, key, value, false)
@@ -91,10 +76,10 @@ func TestToolchainGate(t *testing.T) {
key, value := gateReport(t, "ace", "v26.12.0", old) key, value := gateReport(t, "ace", "v26.12.0", old)
gate.Toolchain(NodeBuildModule, key, value, false) gate.Toolchain(NodeBuildModule, key, value, false)
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out
t.Fatalf("before the placement is settled every report counts; got %+v", a) t.Fatalf("before the assignments is settled every report counts; got %+v", a)
} }
k, v := placing(t, "ace", GoBuildModule) // another build module on ace: no matter k, v := placing(t, "ace", GoBuildModule) // another build module on ace: no matter
gate.Placement(k, v, false, old) gate.Assignments(k, v, false, old)
if !gate.Settled() { if !gate.Settled() {
t.Fatal("settling left ace's report out, so what was passed on changed") t.Fatal("settling left ace's report out, so what was passed on changed")
} }
@@ -109,13 +94,13 @@ func TestToolchainGate(t *testing.T) {
} }
// Placed on ace with a report written since: it counts, and the builders really disagree. // Placed on ace with a report written since: it counts, and the builders really disagree.
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule) k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
gate.Placement(k, v, false, old.Add(-time.Minute)) gate.Assignments(k, v, false, old.Add(-time.Minute))
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" { if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" {
t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a) t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a)
} }
// Taken off ace again (its key deleted): its report goes again. // Taken off ace again (its key deleted): its report goes again.
if !gate.Placement(k, nil, true, placed) { if !gate.Assignments(k, nil, true, placed) {
t.Fatal("taking ace's placement away passed nothing on") t.Fatal("taking ace's assignments away passed nothing on")
} }
if v, none := agreedOf(set); v != "v26.12.1" || none != "" { if v, none := agreedOf(set); v != "v26.12.1" || none != "" {
t.Fatalf("got %q, %q", v, none) t.Fatalf("got %q, %q", v, none)
@@ -132,26 +117,26 @@ func TestToolchainGate(t *testing.T) {
at time.Time // when ace wrote its report at time.Time // when ace wrote its report
counts bool counts bool
}{ }{
{"a report left before the placement", old, false}, {"a report left before the assignments", old, false},
{"a report written since the placement", placed.Add(30 * time.Second), true}, {"a report written since the assignments", placed.Add(30 * time.Second), true},
} { } {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
gate, set, said := start(t, true) gate, set, said := start(t, true)
k, v := placing(t, "ace") // ace's key, first heard placing nothing k, v := placing(t, "ace") // ace's key, first heard placing nothing
gate.Placement(k, v, false, old.Add(-time.Hour)) gate.Assignments(k, v, false, old.Add(-time.Hour))
gate.Settled() gate.Settled()
k, v = placing(t, "ace", NodeBuildModule) k, v = placing(t, "ace", NodeBuildModule)
if gate.Placement(k, v, false, placed) { if gate.Assignments(k, v, false, placed) {
t.Fatal("a placement with no report passed something on") t.Fatal("assignments with no report passed something on")
} }
key, value := gateReport(t, "ace", "v26.12.0", c.at) key, value := gateReport(t, "ace", "v26.12.0", c.at)
if got := gate.Toolchain(NodeBuildModule, key, value, false); got != c.counts { if got := gate.Toolchain(NodeBuildModule, key, value, false); got != c.counts {
t.Fatalf("ace's report passed on: %v, want %v", got, c.counts) t.Fatalf("ace's report passed on: %v, want %v", got, c.counts)
} }
// ace's placement written again with another module beside: its standing does not move, nothing is said again. // ace's assignments written again with another module beside: its standing does not move, nothing is said again.
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule) k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
if gate.Placement(k, v, false, placed.Add(time.Minute)) { if gate.Assignments(k, v, false, placed.Add(time.Minute)) {
t.Fatal("the same placement of mesh-build-node changed what was passed on") t.Fatal("the same assignments of mesh-build-node changed what was passed on")
} }
if c.counts { if c.counts {
return return
@@ -169,47 +154,38 @@ func TestToolchainGate(t *testing.T) {
}) })
} }
t.Run("no placement held: every report counts", func(t *testing.T) { t.Run("no assignments held: every report counts", func(t *testing.T) {
gate, set, _ := start(t, false) gate, set, _ := start(t, false)
key, value := gateReport(t, "ace", "v26.12.0", old) key, value := gateReport(t, "ace", "v26.12.0", old)
gate.Toolchain(NodeBuildModule, key, value, false) gate.Toolchain(NodeBuildModule, key, value, false)
gate.Settled() gate.Settled()
if n := len(set.Entries("typescript")); n != 4 { if n := len(set.Entries("typescript")); n != 4 {
t.Fatalf("before the first placement file every report counts; got %d", n) t.Fatalf("before the first assignments file every report counts; got %d", n)
} }
k, v := placing(t, "g14", NodeBuildModule) k, v := placing(t, "g14", NodeBuildModule)
if !gate.Placement(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 { if !gate.Assignments(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 {
t.Fatalf("the first placement leaves every node it does not place out; got %d", len(set.Entries("typescript"))) t.Fatalf("the first assignments leaves every node it does not place out; got %d", len(set.Entries("typescript")))
} }
if !gate.Placement(k, nil, true, placed) || len(set.Entries("typescript")) != 4 { if !gate.Assignments(k, nil, true, placed) || len(set.Entries("typescript")) != 4 {
t.Fatal("the last placement gone: every report counts again") t.Fatal("the last assignments gone: every report counts again")
} }
}) })
t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) { t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) {
p := meshmoduleassignerclient.NewPlaced() p := meshmoduleassignerclient.NewPlaced()
// ace in the form before, then converted: its module stands since before, the legacy key read no more. // ace's node key is the first revision heard of it: its module stands since before.
k, v := placingLegacy(t, "ace", NodeBuildModule) k, v := placing(t, "ace", NodeBuildModule)
p.Put(k, v, false, old) p.Put(k, v, false, old)
k, v = placing(t, "ace", NodeBuildModule)
if _, changed, err := p.Put(k, v, false, placed); len(changed) != 0 || err != nil {
t.Errorf("the conversion of ace's key changed %v, %v", changed, err)
}
if _, changed, _ := p.Put("ace."+NodeBuildModule, []byte("{}"), false, placed); len(changed) != 0 { if _, changed, _ := p.Put("ace."+NodeBuildModule, []byte("{}"), false, placed); len(changed) != 0 {
t.Errorf("a module key says which modules stand: %v", changed) t.Errorf("a module key says which modules stand: %v", changed)
} }
if k, v := placingLegacy(t, "ace"); true {
if _, changed, _ := p.Put(k, v, false, placed); len(changed) != 0 {
t.Errorf("the legacy key is read after the node key: %v", changed)
}
}
k2, v2 := placing(t, "g14") k2, v2 := placing(t, "g14")
p.Put(k2, v2, false, old) p.Put(k2, v2, false, old)
k2, v2 = placing(t, "g14", NodeBuildModule) k2, v2 = placing(t, "g14", NodeBuildModule)
if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != NodeBuildModule { if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != NodeBuildModule {
t.Errorf("a module placed says changed %v", changed) t.Errorf("a module placed says changed %v", changed)
} }
if _, _, err := p.Put("placement.novox", []byte("not json"), false, old); err == nil { if _, _, err := p.Put("novox.node", []byte("not json"), false, old); err == nil {
t.Error("a value that does not read was not said") t.Error("a value that does not read was not said")
} }
if s, ok := p.Since("ace", NodeBuildModule); !ok || !s.IsZero() { if s, ok := p.Since("ace", NodeBuildModule); !ok || !s.IsZero() {
@@ -223,14 +199,14 @@ func TestToolchainGate(t *testing.T) {
} }
}) })
t.Run("a placement the bus refuses is said, and the part goes on", func(t *testing.T) { t.Run("assignments the bus refuses is said, and the part goes on", func(t *testing.T) {
var logs bytes.Buffer var logs bytes.Buffer
logger := log.New(&logs, "", 0) logger := log.New(&logs, "", 0)
refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement") refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments")
gate, _, _ := start(t, false) gate, _, _ := start(t, false)
for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} { for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} {
if _, err := FollowPlacement(context.Background(), bus, gate, func(context.Context) {}, logger.Printf); err != nil { if _, err := FollowAssignments(context.Background(), bus, gate, func(context.Context) {}, logger.Printf); err != nil {
t.Errorf("a refused placement stopped the start: %v", err) t.Errorf("a refused assignments stopped the start: %v", err)
} }
} }
if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") { if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") {
@@ -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...) 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...) 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.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1) 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 // 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 { if err != nil {
return err 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 // 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 // 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. // 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 { if err != nil {
return err 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 { if err != nil {
return err 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. // The toolchains first, so a build that starts from the first modules value finds the nodes.
watch := newToolchainWatch(ctx, js, tools, b.NodesChanged, logger) 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 // 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) gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
watch.put = gate.Toolchain 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 // 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) 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 // 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. // 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 gate.OnPlaced = reads.placed
b.Forms = reads.built b.Forms = reads.built
// Until the placement's first view is answered the gate passes every report on, as before the placement is settled. // Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
placementErrs := make(chan error, 1) assignmentsErrs := make(chan error, 1)
go func() { go func() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return 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 { if err != nil {
placementErrs <- err assignmentsErrs <- err
return return
} }
placementErrs <- <-pe assignmentsErrs <- <-pe
}() }()
if err := watch.start(); err != nil { if err := watch.start(); err != nil {
return err return err
@@ -456,7 +456,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return nil return nil
case err := <-errs: case err := <-errs:
return err return err
case err := <-placementErrs: case err := <-assignmentsErrs:
return err return err
case err := <-watch.errs: case err := <-watch.errs:
return err return err
@@ -16,7 +16,7 @@ const (
// indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows. // indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows.
indexerModule = "mesh-delivery-indexer" indexerModule = "mesh-delivery-indexer"
// assignerModule is the holder of the state placement. // assignerModule is the holder of the state assignments.
assignerModule = buildrun.AssignerModule assignerModule = buildrun.AssignerModule
// sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names. // 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", 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) { if !slices.Contains(m.Consumes, view) {
t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes) 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{ for want, got := range map[string]string{
"mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"), "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-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 { if want != got {
t.Errorf("a bucket is %q, want %q", got, want) 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 // 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 // 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. // as version.old-form-closed and in the builder's log.
type oldForm struct { type oldForm struct {
@@ -84,7 +84,7 @@ func (o *oldForm) dropped() map[string]bool {
} }
// step moves the window to what counted states. // 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. // 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) { func (o *oldForm) step(ctx context.Context, named []string, counted map[string][]string) {
o.mu.Lock() 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)), n, next.Since[n].Format(time.RFC3339)),
headline: "A node holds the old reference form", 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.", 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."}) 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 o.overdue = overdue
@@ -10,14 +10,14 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/process" "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-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 // 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 // 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 // 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. // with no key reads as stating the baseline: its first forms. No node's record is read in its place.
const EngineReadsState = "engine-reads" 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 // 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() { func (r *engineReads) stepWindow() {
if r.window == nil { if r.window == nil {
return return
@@ -56,7 +56,7 @@ func (r *engineReads) stepWindow() {
r.window.step(ctx, r.named(), counted) 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 { func (r *engineReads) named() []string {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() 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). // first call asks every counted node (the builder's start).
func (r *engineReads) placed() { func (r *engineReads) placed() {
nodes := r.gate.Nodes() nodes := r.gate.Nodes()
@@ -96,7 +96,7 @@ func (r *engineReads) placed() {
r.mu.Unlock() r.mu.Unlock()
for _, node := range nodes { for _, node := range nodes {
if first || !slices.Contains(before, node) { 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 { for _, node := range before {
@@ -20,15 +20,15 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "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/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" 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/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "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-builder/builds/forms"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "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/build"
"mesh-delivery-builder/internal/versions" "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 // 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 // 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). // issue 774).
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) { func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard) 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) 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 // 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. // builder_engine_reads drops one node's key.
func theCountedNodesEngineReads(t *testing.T) { func theCountedNodesEngineReads(t *testing.T) {
gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf) gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf)
place := func(nodes ...string) { place := func(nodes ...string) {
for _, node := range []string{"n1", "n2"} { for _, node := range []string{"n1", "n2"} {
pl := meshmoduleassigner.Placement{Node: node} pl := meshmoduleassigner.Assignments{Node: node}
if slices.Contains(nodes, node) { if slices.Contains(nodes, node) {
pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}} pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}}
} }
values, err := meshmoduleassignerclient.PlacementValues(pl) values, err := meshmoduleassignerclient.AssignmentsValues(pl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for key, raw := range values { for key, raw := range values {
gate.Placement(key, raw, false, time.Now()) gate.Assignments(key, raw, false, time.Now())
} }
if !slices.Contains(nodes, node) { if !slices.Contains(nodes, node) {
key, _ := meshmoduleassignerclient.ModuleKey(node, "web") 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 { 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"]) 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") place("n1")
r.placed() r.placed()
if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil { if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil {
+2 -2
View File
@@ -10,8 +10,8 @@ require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
@@ -18,8 +18,8 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "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"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
repotrunk "git.novox.be/novox/mesh-delivery/sdk/trunk" repotrunk "git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -92,7 +92,7 @@ type Builder struct {
// Tags answers a check's build's tag from the kept tag asks; nil numbers no check's build. // Tags answers a check's build's tag from the kept tag asks; nil numbers no check's build.
Tags TagOf Tags TagOf
// Forms answers what each node's engine states it reads (ADR 0618 §2, ADR 0624 §1): present, the nodes whose engine // Forms answers what each node's engine states it reads (ADR 0618 §2, ADR 0624 §1): present, the nodes whose engine
// answered when last asked, and counted, every node the placement names, present or away, a node with no statement // answered when last asked, and counted, every node the assignments names, present or away, a node with no statement
// stating nothing (the baseline's first forms). A done key names each artifact in the forms these keep, the oldest in // stating nothing (the baseline's first forms). A done key names each artifact in the forms these keep, the oldest in
// its single reference, and its needs. Nil: no node states anything, and the first forms are kept. // its single reference, and its needs. Nil: no node states anything, and the first forms are kept.
Forms func() (present, counted map[string][]string) Forms func() (present, counted map[string][]string)
@@ -19,7 +19,7 @@ import (
// builder's state engine-reads; this is only the window over them: open while a counted node-engine, present or away, // builder's state engine-reads; this is only the window over them: open while a counted node-engine, present or away,
// reads of some artifact kind none of the newest form the builder knows for it, so an older form stays in new build keys // reads of some artifact kind none of the newest form the builder knows for it, so an older form stays in new build keys
// for it; closed, said once with `old-form-closed` and a log line, when none does. It moves on what moves a statement // for it; closed, said once with `old-form-closed` and a log line, when none does. It moves on what moves a statement
// (the node-engine seat's verb `manifest-reads` and its event `manifest-reads-changed`), the placement, a hand drop and // (the node-engine seat's verb `manifest-reads` and its event `manifest-reads-changed`), the assignments, a hand drop and
// each build key written; never on a timer. // each build key written; never on a timer.
// OldFormState is the builder's state of the window: one key, OldFormKey, an OldFormWindow. builder_old_form reads it. // OldFormState is the builder's state of the window: one key, OldFormKey, an OldFormWindow. builder_old_form reads it.
@@ -94,7 +94,7 @@ const FindingAfter = 7 * 24 * time.Hour
// Step moves the window by mesh-sdk's one window rule (window.StepWindow): named is every counted node the read named, // Step moves the window by mesh-sdk's one window rule (window.StepWindow): named is every counted node the read named,
// those dropped by hand included, and holding those of them holding the window (Holding). A read naming no node (no // those dropped by hand included, and holding those of them holding the window (Holding). A read naming no node (no
// placement read yet, an empty node_haves) judges nothing, so it never closes the window. It answers the new window, // assignments read yet, an empty node_haves) judges nothing, so it never closes the window. It answers the new window,
// whether it closed in this step (so the builder says `old-form-closed` and its log line once), and the nodes holding it // whether it closed in this step (so the builder says `old-form-closed` and its log line once), and the nodes holding it
// past FindingAfter (the finding, cleared for every other node). // past FindingAfter (the finding, cleared for every other node).
func (w OldFormWindow) Step(named, holding []string, now time.Time) (next OldFormWindow, closed bool, overdue []string) { func (w OldFormWindow) Step(named, holding []string, now time.Time) (next OldFormWindow, closed bool, overdue []string) {
@@ -146,7 +146,7 @@ const OldFormDescription = "The old reference form's window (design 51 *Every bu
"the counted nodes, present or away, whose node-engine reads an older reference form alone and since when (a node holding it seven days " + "the counted nodes, present or away, whose node-engine reads an older reference form alone and since when (a node holding it seven days " +
"is the builder's finding " + OldFormHeld + "), when it last closed and who held it then, and the retired nodes dropped from the count by " + "is the builder's finding " + OldFormHeld + "), when it last closed and who held it then, and the retired nodes dropped from the count by " +
"hand. With drop (a node) and why, drops that retired node from the count by hand, a hand act the builder answers; a node still placed " + "hand. With drop (a node) and why, drops that retired node from the count by hand, a hand act the builder answers; a node still placed " +
"leaves the count by itself only when the placement no longer names it. builder_engine_reads shows what each node's engine states." "leaves the count by itself only when the assignments no longer names it. builder_engine_reads shows what each node's engine states."
// AnswerOldForm is builder_old_form's read: the window as the state holds it. // AnswerOldForm is builder_old_form's read: the window as the state holds it.
func AnswerOldForm(r Reader) (OldFormWindow, error) { func AnswerOldForm(r Reader) (OldFormWindow, error) {
@@ -365,7 +365,7 @@ func TestTheBuildersVersions(t *testing.T) {
t.Error("a drop with no why is taken") t.Error("a drop with no why is taken")
} }
// Security-upgrade F3 (review round 1), by mesh-sdk's window.StepWindow: a read naming no node (an empty // Security-upgrade F3 (review round 1), by mesh-sdk's window.StepWindow: a read naming no node (an empty
// node_haves, the placement not read yet) never closes the window, and its holder's seven days still count. // node_haves, the assignments not read yet) never closes the window, and its holder's seven days still count.
if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) { if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) {
t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late) t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late)
} }
@@ -102,7 +102,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
// 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and // 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and
// holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5). // holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5).
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains", if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains",
"mesh-delivery-assigner.placement", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { "mesh-delivery-assigner.assignments", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want) t.Errorf("reads = %v, want %v", m.Reads, want)
} }
if want := []string{"node-build-go", "node-build-image", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { if want := []string{"node-build-go", "node-build-image", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
@@ -111,18 +111,18 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no // 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
// other tool; it holds no capacity. // other tool; it holds no capacity.
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955). // 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955).
// DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's placement are asked by their // DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's assignments are asked by their
// read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is // read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is
// numbered under (design 32). // numbered under (design 32).
for _, r := range m.Reads { for _, r := range m.Reads {
// The toolchain gate follows the assigner's placement through meshmoduleassignerclient (buildrun.PlacementBucket), so // The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so
// it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools. // it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools.
if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.placement") { if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.assignments") {
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r) t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
} }
} }
// Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements. // Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements.
if want := []string{"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
"seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove",
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 { "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
@@ -137,7 +137,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed", if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed",
"go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed", "go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
"mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-catalog.manifest-accepted", "mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-catalog.manifest-accepted",
"mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.placement", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**", "mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.assignments", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**",
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.packages", "node-engine.manifest-reads-changed", "mesh-delivery-indexer.views.packages", "node-engine.manifest-reads-changed",
"npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed", "npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
+3 -3
View File
@@ -52,7 +52,7 @@
"oci-artifact-registry.published", "oci-artifact-registry.published",
"oci-artifact-registry.refused", "oci-artifact-registry.refused",
"oci-artifact-registry.removed", "oci-artifact-registry.removed",
"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.candidates",
"mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.manifests",
"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.modules",
@@ -72,7 +72,7 @@
"seat:npm-package-registry.versions", "seat:npm-package-registry.versions",
"seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.remove",
"seat:oci-artifact-registry.versions", "seat:oci-artifact-registry.versions",
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_candidates",
"mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_modules",
@@ -103,7 +103,7 @@
"mesh-build-image.steps", "mesh-build-image.steps",
"mesh-build-node.steps", "mesh-build-node.steps",
"mesh-build-node.toolchains", "mesh-build-node.toolchains",
"mesh-delivery-assigner.placement", "mesh-delivery-assigner.assignments",
"mesh-delivery-gate.source" "mesh-delivery-gate.source"
], ],
"uses": [ "uses": [
@@ -15,9 +15,9 @@ import (
"mesh-delivery-checker/internal/check" "mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
@@ -58,7 +58,7 @@ func (e engineAsks) ComposeCandidate(ctx context.Context, node string, ask ne.Co
} }
} }
// placedNodes names every node the seat mesh-placement places now, read from its state when a check composes (no // placedNodes names every node the seat mesh-module-assigner places now, read from its state when a check composes (no
// watch: a check reads it once). A state that cannot be read is an error, so the check says its composition was not // watch: a check reads it once). A state that cannot be read is an error, so the check says its composition was not
// made rather than passing with no node asked (review finding 4). // made rather than passing with no node asked (review finding 4).
type placedNodes struct { type placedNodes struct {
@@ -70,14 +70,14 @@ func (p placedNodes) Placed() ([]string, error) {
defer cancel() defer cancel()
kv, err := p.kv(ctx) kv, err := p.kv(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.PlacementBucket, err) return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.AssignmentsBucket, err)
} }
lister, err := kv.ListKeys(ctx) lister, err := kv.ListKeys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) { if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil // a placement that places nothing return nil, nil // assignments that places nothing
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.PlacementBucket, err) return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.AssignmentsBucket, err)
} }
seen := map[string]bool{} seen := map[string]bool{}
for key := range lister.Keys() { for key := range lister.Keys() {
@@ -25,13 +25,13 @@ import (
"mesh-delivery-checker/internal/check" "mesh-delivery-checker/internal/check"
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
@@ -323,13 +323,13 @@ func (b *bed) said(t *testing.T, words string) {
// the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked. // the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked.
func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput { func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput {
t.Helper() t.Helper()
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.PlacementBucket, History: 1}) kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.AssignmentsBucket, History: 1})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, n := range nodes { for _, n := range nodes {
pk, _ := meshmoduleassignerclient.NodeKey(n) pk, _ := meshmoduleassignerclient.NodeKey(n)
raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.PlacementByNodeNodeValue{Node: n}) raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.AssignmentsByNodeNodeValue{Node: n})
if _, err := kv.Put(b.ctx, pk, raw); err != nil { if _, err := kv.Put(b.ctx, pk, raw); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -466,7 +466,7 @@ func startBed(t *testing.T) *bed {
for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} { for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} {
viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule) viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule)
} }
viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StatePlacement, buildrun.AssignerModule+".assigner_read_placement", "n1_"+check.CheckerModule) viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StateAssignments, buildrun.AssignerModule+".assigner_read_assignments", "n1_"+check.CheckerModule)
// 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone // 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone
// judges the repository (an unknown repository is judged by both, issue 1887 A12). // judges the repository (an unknown repository is judged by both, issue 1887 A12).
known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"}) known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"})
@@ -25,7 +25,7 @@ import (
"mesh-delivery-checker/internal/check" "mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
@@ -145,7 +145,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
logger.Printf("what the checker keeps could not be said on the bus, so readers of owed, previews and verdicts are not granted until it is: %v", err) logger.Printf("what the checker keeps could not be said on the bus, so readers of owed, previews and verdicts are not granted until it is: %v", err)
} }
// The candidates it checks, the modules and manifests that say which build modules judge a repository, the published // The candidates it checks, the modules and manifests that say which build modules judge a repository, the published
// packages, the predefined keys and the placement are their owners' states (the indexer's, the assigner's), read by the // packages, the predefined keys and the assignments are their owners' states (the indexer's, the assigner's), read by the
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2). // first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2).
// An owner that is down never holds the start: the checks start once every first view is answered, and until then the // An owner that is down never holds the start: the checks start once every first view is answered, and until then the
// unanswered first view is the checker's finding. // unanswered first view is the checker's finding.
@@ -168,7 +168,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
manifests := followOwner(check.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews) manifests := followOwner(check.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews)
packages := followOwner(check.IndexerModule, meshcatalog.StatePackages, "indexer_read_packages", indexerViews) packages := followOwner(check.IndexerModule, meshcatalog.StatePackages, "indexer_read_packages", indexerViews)
predefined := followOwner(check.IndexerModule, check.IndexerPredefinedState, "indexer_read_predefined", indexerViews) predefined := followOwner(check.IndexerModule, check.IndexerPredefinedState, "indexer_read_predefined", indexerViews)
placement := followOwner(buildrun.AssignerModule, meshmoduleassigner.StatePlacement, "assigner_read_placement", assignerViews) assignments := followOwner(buildrun.AssignerModule, meshmoduleassigner.StateAssignments, "assigner_read_assignments", assignerViews)
// The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails. // The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails.
seatDefs := followOwner(check.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews) seatDefs := followOwner(check.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews)
if followed != nil { if followed != nil {
@@ -246,7 +246,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
verdicts := &manifestVerdicts{} verdicts := &manifestVerdicts{}
compositions := check.EngineCompositions{ compositions := check.EngineCompositions{
Asker: engineAsks{call: caller.CallOn}, Asker: engineAsks{call: caller.CallOn},
Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return placement, nil }}.Placed, Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return assignments, nil }}.Placed,
Bound: answerBound, Bound: answerBound,
Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}}, Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}},
Verdicts: verdicts.all, Verdicts: verdicts.all,
@@ -303,23 +303,23 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The toolchains first, so a check that starts from the first candidate finds the nodes. // The toolchains first, so a check that starts from the first candidate finds the nodes.
watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger) watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger)
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the // 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) gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
watch.put = gate.Toolchain watch.put = gate.Toolchain
// Until the placement's first view is answered the gate passes every report on, as before the placement is settled. // Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
placementErrs := make(chan error, 1) assignmentsErrs := make(chan error, 1)
go func() { go func() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-placement.Follower().Ready(): case <-assignments.Follower().Ready():
} }
pe, err := buildrun.FollowPlacement(ctx, reader, gate, ch.NodesChanged, logger.Printf) pe, err := buildrun.FollowAssignments(ctx, reader, gate, ch.NodesChanged, logger.Printf)
if err != nil { if err != nil {
placementErrs <- err assignmentsErrs <- err
return return
} }
placementErrs <- <-pe assignmentsErrs <- <-pe
}() }()
if err := watch.start(); err != nil { if err := watch.start(); err != nil {
return err return err
@@ -481,7 +481,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
defer hub.Stop() defer hub.Stop()
// The checks, the tries and the hand asks, once every first view is answered (the lead's ruling for DLV-M2): a check // The checks, the tries and the hand asks, once every first view is answered (the lead's ruling for DLV-M2): a check
// judged without the candidates, the modules, the manifests, the published packages, the predefined keys or the // judged without the candidates, the modules, the manifests, the published packages, the predefined keys or the
// placement would be another check. // assignments would be another check.
go func() { go func() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -500,7 +500,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return nil return nil
case err := <-errs: case err := <-errs:
return err return err
case err := <-placementErrs: case err := <-assignmentsErrs:
return err return err
case err := <-watch.errs: case err := <-watch.errs:
return err return err
@@ -17,7 +17,7 @@ import (
// 1945 (a part exits at start for a bucket it works without): a bus that refuses every toolchains bucket leaves the // 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 // 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). // issue 774).
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) { func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
set := &buildrun.ToolchainSet{} set := &buildrun.ToolchainSet{}
+3 -3
View File
@@ -5,9 +5,9 @@ go 1.26.0
require ( require (
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
@@ -3,9 +3,9 @@ package check
import ( import (
"strings" "strings"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
) )
// withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine. // withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine.
@@ -8,10 +8,10 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
) )
@@ -12,10 +12,10 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-delivery/sdk/composer"
) )
@@ -46,7 +46,7 @@ type Sandbox interface {
// EngineCompositions is the checker's composition of a check (Examiner.Compositions). // EngineCompositions is the checker's composition of a check (Examiner.Compositions).
type EngineCompositions struct { type EngineCompositions struct {
Asker EngineAsker Asker EngineAsker
// Placed names every node in placement now (the seat mesh-placement's state), or why it cannot be read: then no node // Placed names every node in assignments now (the seat mesh-module-assigner's state), or why it cannot be read: then no node
// can be asked, and the composition is not made (an error), never passed with nobody asked. // can be asked, and the composition is not made (an error), never passed with nobody asked.
Placed func() ([]string, error) Placed func() ([]string, error)
// Bound is the answer bound (the setting compose-answer-bound); zero is nec.AnswerBound. // Bound is the answer bound (the setting compose-answer-bound); zero is nec.AnswerBound.
@@ -80,7 +80,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.Com
var nodes []string var nodes []string
if e.Placed != nil { if e.Placed != nil {
if nodes, err = e.Placed(); err != nil { if nodes, err = e.Placed(); err != nil {
return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err) return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the assignments cannot be read, so no node can be asked: %w", err)
} }
} }
bound := e.Bound bound := e.Bound
@@ -12,10 +12,10 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-delivery/sdk/composer"
) )
@@ -204,11 +204,11 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
} }
}) })
// Review finding 4: a placement that cannot be read is no mesh with no node: the composition is not made, said. // Review finding 4: assignments that cannot be read is no mesh with no node: the composition is not made, said.
t.Run("an unreadable placement makes no composition", func(t *testing.T) { t.Run("an unreadable assignments makes no composition", func(t *testing.T) {
ec := EngineCompositions{Asker: &fakeEngines{}, Placed: func() ([]string, error) { return nil, errors.New("no such bucket") }} ec := EngineCompositions{Asker: &fakeEngines{}, Placed: func() ([]string, error) { return nil, errors.New("no such bucket") }}
if _, err := ec.Compose(context.Background(), cs); err == nil || !strings.Contains(err.Error(), "no such bucket") { if _, err := ec.Compose(context.Background(), cs); err == nil || !strings.Contains(err.Error(), "no such bucket") {
t.Errorf("an unreadable placement: %v", err) t.Errorf("an unreadable assignments: %v", err)
} }
b := newBench(t) b := newBench(t)
b.ex.Compositions = ec b.ex.Compositions = ec
@@ -6,9 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
) )
@@ -20,12 +20,12 @@ import (
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig" "git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "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"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
@@ -10,10 +10,10 @@ import (
"strings" "strings"
"time" "time"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
) )
@@ -11,10 +11,10 @@ import (
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg"
"git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/buildstep"
) )
@@ -115,7 +115,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
t.Errorf("reads = %v, want %v", m.Reads, want) t.Errorf("reads = %v, want %v", m.Reads, want)
} }
// DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the // DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the
// assigner's placement are asked by their read tools and followed by their views families, never read. // assigner's assignments are asked by their read tools and followed by their views families, never read.
for _, r := range m.Reads { for _, r := range m.Reads {
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") { if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r) t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
@@ -126,7 +126,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
// The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb // The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb
// compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs. // compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs.
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches", if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_seat_definitions", "mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
@@ -138,7 +138,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
// And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*). // And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*).
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want) t.Errorf("consumes = %v, want %v", m.Consumes, want)
} }
+2 -2
View File
@@ -19,7 +19,7 @@
"mesh-catalog.seat-definition-removed", "mesh-catalog.seat-definition-removed",
"mesh-build-go.toolchain.reported", "mesh-build-go.toolchain.reported",
"mesh-build-node.toolchain.reported", "mesh-build-node.toolchain.reported",
"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.candidates",
"mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.manifests",
"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.modules",
@@ -77,7 +77,7 @@
"gitea.gitea_compare", "gitea.gitea_compare",
"gitea.gitea_get_file", "gitea.gitea_get_file",
"gitea.gitea_list_branches", "gitea.gitea_list_branches",
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-assigner.assigner_read_assignments",
"mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_candidates",
"mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_modules",
@@ -143,11 +143,11 @@ type Forge struct {
Act string `json:"act,omitempty"` Act string `json:"act,omitempty"`
Words string `json:"words,omitempty"` Words string `json:"words,omitempty"`
Open []OpenPull `json:"open,omitempty"` Open []OpenPull `json:"open,omitempty"`
// RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is // RetiredAssignments is the assignments files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): assignments is
// the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is // the seat mesh-module-assigner's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
// refused, loudly, rather than read with them dropped. // refused, loudly, rather than read with them dropped.
RetiredPlacements json.RawMessage `json:"placements,omitempty"` RetiredAssignments json.RawMessage `json:"assignments,omitempty"`
RetiredRead bool `json:"placements-read,omitempty"` RetiredRead bool `json:"assignments-read,omitempty"`
// Predefined is the catalogue's predefined.json, exact, at the merge commit of a merged message (novox/hq ADR 0565 // Predefined is the catalogue's predefined.json, exact, at the merge commit of a merged message (novox/hq ADR 0565
// §2): what the indexer keeps as its state `predefined`. PredefinedRead says the bridge read the merge and it holds // §2): what the indexer keeps as its state `predefined`. PredefinedRead says the bridge read the merge and it holds
// no predefined.json. Carried on a merged message only; an older bridge sends neither, and the indexer keeps what it // no predefined.json. Carried on a merged message only; an older bridge sends neither, and the indexer keeps what it
@@ -226,9 +226,9 @@ func (f Forge) check() error {
if len(f.Open) > 0 { if len(f.Open) > 0 {
return fmt.Errorf("delivery: only an open message names open pull requests") return fmt.Errorf("delivery: only an open message names open pull requests")
} }
if len(f.RetiredPlacements) > 0 || f.RetiredRead { if len(f.RetiredAssignments) > 0 || f.RetiredRead {
return fmt.Errorf("delivery: a forge message carries placement files, which no repository holds since ADR 0567: " + return fmt.Errorf("delivery: a forge message carries assignments files, which no repository holds since ADR 0567: " +
"placement is the seat mesh-placement's state") "assignments is the seat mesh-module-assigner's state")
} }
if err := f.checkPredefined(); err != nil { if err := f.checkPredefined(); err != nil {
return err return err
@@ -148,8 +148,8 @@ func TestForge(t *testing.T) {
{"empty lock hash", `"a"`, in(func(f *Forge) { f.Locks["a"]["go.sum"] = "" })}, {"empty lock hash", `"a"`, in(func(f *Forge) { f.Locks["a"]["go.sum"] = "" })},
{"named paths outside trees", `"other"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"other": {"src": "o"}} })}, {"named paths outside trees", `"other"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"other": {"src": "o"}} })},
{"a named path with no object", `"src"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"a": {"src": ""}} })}, {"a named path with no object", `"src"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"a": {"src": ""}} })},
{"placement files", "mesh-placement", in(func(f *Forge) { f.RetiredPlacements = []byte(`{"anchor":"{}"}`) })}, {"assignments files", "mesh-module-assigner", in(func(f *Forge) { f.RetiredAssignments = []byte(`{"anchor":"{}"}`) })},
{"placements read", "mesh-placement", in(func(f *Forge) { f.RetiredRead = true })}, {"assignments read", "mesh-module-assigner", in(func(f *Forge) { f.RetiredRead = true })},
{"predefined not an object", "one JSON object", in(func(f *Forge) { f.Predefined = []byte(`["domain"]`) })}, {"predefined not an object", "one JSON object", in(func(f *Forge) { f.Predefined = []byte(`["domain"]`) })},
{"predefined both", "says the merge holds none", in(func(f *Forge) { f.Predefined = []byte(`{}`); f.PredefinedRead = true })}, {"predefined both", "says the merge holds none", in(func(f *Forge) { f.Predefined = []byte(`{}`); f.PredefinedRead = true })},
{"pull with the predefined list", "only a merged message", Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, PredefinedRead: true}}, {"pull with the predefined list", "only a merged message", Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, PredefinedRead: true}},
@@ -84,7 +84,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
if len(f.Packages) != 0 || strings.Contains(string(msg.Data), `"packages"`) { if len(f.Packages) != 0 || strings.Contains(string(msg.Data), `"packages"`) {
t.Errorf("the merged message carries packages: %v", f.Packages) t.Errorf("the merged message carries packages: %v", f.Packages)
} }
// ADR 0567: placement is the seat mesh-placement's: a merge of the catalogue carries no placement file and asks the // ADR 0567: assignments is the seat mesh-module-assigner's: a merge of the catalogue carries no assignments file and asks the
// forge for none. // forge for none.
forge.calls = nil forge.calls = nil
msg, err = Convert(mergedSubject, mergedBody(map[string]any{"repo": "mesh-catalog"}), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get) msg, err = Convert(mergedSubject, mergedBody(map[string]any{"repo": "mesh-catalog"}), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get)
@@ -94,11 +94,11 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
if f, err = forgemsg.DecodeForge(msg.Data); err != nil { if f, err = forgemsg.DecodeForge(msg.Data); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(f.RetiredPlacements) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"placements`) { if len(f.RetiredAssignments) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"assignments`) {
t.Errorf("a merge of the catalogue carries placement: %s", msg.Data) t.Errorf("a merge of the catalogue carries assignments: %s", msg.Data)
} }
for _, c := range forge.calls { for _, c := range forge.calls {
if strings.HasPrefix(c.path, "placement") { if strings.HasPrefix(c.path, "assignments") {
t.Errorf("the forge was asked for %s", c.path) t.Errorf("the forge was asked for %s", c.path)
} }
} }
@@ -25,7 +25,7 @@ func holdsTool(read stdio.KeptState, ask asker, now func() time.Time, acts meshh
") which not every counted node-engine states, present or away (ADR 0624 §1). Per module: what it needs, which nodes " + ") which not every counted node-engine states, present or away (ADR 0624 §1). Per module: what it needs, which nodes " +
"lack what (or why no node is counted yet), the manifest the modules state keeps and the one held, the merge that " + "lack what (or why no node is counted yet), the manifest the modules state keeps and the one held, the merge that " +
"brought it and since when. The hold moves by itself once every counted node-engine states it (on manifest-reads-changed, " + "brought it and since when. The hold moves by itself once every counted node-engine states it (on manifest-reads-changed, " +
"the placement's change or a merge). With module and ask_again, the engines of the nodes that lack it are asked again " + "the assignments's change or a merge). With module and ask_again, the engines of the nodes that lack it are asked again " +
"and the hold is weighed now; with module, release and why, it moves now by your word whatever the engines state (a hand " + "and the hold is weighed now; with module, release and why, it moves now by your word whatever the engines state (a hand " +
"act, recorded through mesh-hand-acts, which names the nodes it strands: each node whose engine lacks the reading, an " + "act, recorded through mesh-hand-acts, which names the nodes it strands: each node whose engine lacks the reading, an " +
"away node from its return, reads the new entry without it and an engine before form:seat-reach finds no store). A hold " + "away node from its return, reads the new entry without it and an engine before form:seat-reach finds no store). A hold " +
@@ -29,7 +29,7 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
for _, t := range index.ReadTools { for _, t := range index.ReadTools {
out = append(out, views.ReadTool(index.Module, t.Tool, t.State, t.Words, state(t.State), marks, now)) out = append(out, views.ReadTool(index.Module, t.Tool, t.State, t.Words, state(t.State), marks, now))
} }
// Its own first views, of the assigner's placement (DLV-F3r): read, and asked again by hand. // Its own first views, of the assigner's assignments (DLV-F3r): read, and asked again by hand.
return append(out, views.StatusTools(index.Module, "indexer_first_views", "indexer_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...) return append(out, views.StatusTools(index.Module, "indexer_first_views", "indexer_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...)
} }
@@ -24,8 +24,8 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
@@ -559,7 +559,7 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
b(ctx, js) b(ctx, js)
} }
serveAssignerPlacement(t, ctx, js, ctl) serveAssignerAssignments(t, ctx, js, ctl)
broker := filepath.Join(t.TempDir(), "broker") broker := filepath.Join(t.TempDir(), "broker")
cred, _ := json.Marshal(process.Account{URL: srv.ClientURL(), Node: "n1", Module: "mesh-delivery-indexer", User: "n1.mesh-delivery-indexer", Password: "pw"}) cred, _ := json.Marshal(process.Account{URL: srv.ClientURL(), Node: "n1", Module: "mesh-delivery-indexer", User: "n1.mesh-delivery-indexer", Password: "pw"})
@@ -978,33 +978,33 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
// TestIndexerCountsOnlyThePlacedBuildNodesToolchains (issue 1940, issue 774): the toolchains bucket holds the report of // TestIndexerCountsOnlyThePlacedBuildNodesToolchains (issue 1940, issue 774): the toolchains bucket holds the report of
// n2, where the catalogue no longer places mesh-build-go, at another version than n1's, as ace's v26.12.0 stayed beside // n2, where the catalogue no longer places mesh-build-go, at another version than n1's, as ace's v26.12.0 stayed beside
// the v26.12.1 of the nodes mesh-build-node runs on. The indexer gives the merged module a fingerprint from n1's // the v26.12.1 of the nodes mesh-build-node runs on. The indexer gives the merged module a fingerprint from n1's
// toolchain alone; when the placement puts mesh-build-go on n2 again, its report written since counts and the builders // toolchain alone; when the assignments puts mesh-build-go on n2 again, its report written since counts and the builders
// disagree (the guard stays); when it is taken off again, the fingerprint comes back. No restart, no timer: the // disagree (the guard stays); when it is taken off again, the fingerprint comes back. No restart, no timer: the
// placement's watch (ADR 0549 §3), not the controller's assignments. // assignments's watch (ADR 0549 §3), not the controller's assignments.
func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) { func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json") manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381" const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"
var placement, toolchains jetstream.KeyValue var assignments, toolchains jetstream.KeyValue
place := func(ctx context.Context, t *testing.T, node string, modules ...string) { place := func(ctx context.Context, t *testing.T, node string, modules ...string) {
p := meshmoduleassigner.Placement{Node: node} p := meshmoduleassigner.Assignments{Node: node}
for _, m := range modules { for _, m := range modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m}) p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
} }
// Its node key, the commit that names the modules placed (research 072 row 37). // Its node key, the commit that names the modules placed (research 072 row 37).
values, err := meshmoduleassignerclient.PlacementValues(p) values, err := meshmoduleassignerclient.AssignmentsValues(p)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
key := meshmoduleassigner.PlacementByNodeNodeKey(node) key := meshmoduleassigner.AssignmentsByNodeNodeKey(node)
if _, err := placement.Put(ctx, key, values[key]); err != nil { if _, err := assignments.Put(ctx, key, values[key]); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
// DLV-F5r (ADR 0621 §3): the assigner's runtime starts together with the indexer and answers its first view only after // DLV-F5r (ADR 0621 §3): the assigner's runtime starts together with the indexer and answers its first view only after
// the indexer asked; within the bound the indexer follows the placement it answers (only n1's toolchain counts). // the indexer asked; within the bound the indexer follows the assignments it answers (only n1's toolchain counts).
up := make(chan struct{}) up := make(chan struct{})
assignerLate = up assignerLate = up
t.Cleanup(func() { assignerLate = nil }) t.Cleanup(func() { assignerLate = nil })
@@ -1024,8 +1024,8 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
// The seat mesh-placement's placement, as the assigner writes it (ADR 0567): n1 places mesh-build-go, n2 nothing. // The seat mesh-module-assigner's assignments, as the assigner writes it (ADR 0567): n1 places mesh-build-go, n2 nothing.
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil { if assignments, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.AssignmentsBucket, History: index.AssignmentsHistory}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
place(ctx, t, "n1", "mesh-build-go") place(ctx, t, "n1", "mesh-build-go")
@@ -1072,13 +1072,13 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
if _, err := toolchains.Put(ctx, key, value); err != nil { if _, err := toolchains.Put(ctx, key, value); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// n2's report, written since its placement, counts: the builders disagree. A fingerprint already computed is kept // n2's report, written since its assignments, counts: the builders disagree. A fingerprint already computed is kept
// (issue 1940), and only a new one needs the agreement: a merge moving avahi gets none until they agree. // (issue 1940), and only a new one needs the agreement: a merge moving avahi gets none until they agree.
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
if e, err := modules.Get(ctx, "avahi"); err != nil { if e, err := modules.Get(ctx, "avahi"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if m, err := meshcatalogclient.DecodeModule(e.Value()); err != nil || m.Fp2 != held.Fp2 { } else if m, err := meshcatalogclient.DecodeModule(e.Value()); err != nil || m.Fp2 != held.Fp2 {
t.Fatalf("after n2's placement avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.Fp2, m.None2, held.Fp2) t.Fatalf("after n2's assignments avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.Fp2, m.None2, held.Fp2)
} }
moved := strings.Replace(string(data), `"newtree"`, `"movedtree"`, 1) moved := strings.Replace(string(data), `"newtree"`, `"movedtree"`, 1)
if moved == string(data) { if moved == string(data) {
@@ -1091,11 +1091,11 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
return m.Fp2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2") return m.Fp2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2")
} }
if m := read(disagree); !disagree(m) { if m := read(disagree); !disagree(m) {
t.Fatalf("within 20 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.Fp2, m.None2) t.Fatalf("within 20 s of a merge after n2's assignments avahi has fp2 %q, none2 %q: want the builders to disagree", m.Fp2, m.None2)
} }
place(ctx, t, "n2") place(ctx, t, "n2")
if m := read(agreed); !agreed(m) { if m := read(agreed); !agreed(m) {
t.Fatalf("within 20 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.Fp2, m.None2) t.Fatalf("within 20 s of n2's assignments taken away avahi has fp2 %q, none2 %q", m.Fp2, m.None2)
} }
} }
@@ -1103,14 +1103,14 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
// assigner's runtime started together with the indexer's, answering after the indexer asked (DLV-F5r). // assigner's runtime started together with the indexer's, answering after the indexer asked (DLV-F5r).
var assignerLate chan struct{} var assignerLate chan struct{}
// serveAssignerPlacement is the assigner as the owner of its placement (ADR 0615, ADR 0621), with the real owner's side: // serveAssignerAssignments is the assigner as the owner of its assignments (ADR 0615, ADR 0621), with the real owner's side:
// its first-view tool placement answered from the bucket at its mark (the bucket made empty where a test made none), // its first-view tool assignments answered from the bucket at its mark (the bucket made empty where a test made none),
// and its views family said by the views emitter onto ASSIGNER_VIEWS, which the indexer's durable consumer reads. // and its views family said by the views emitter onto ASSIGNER_VIEWS, which the indexer's durable consumer reads.
func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) { func serveAssignerAssignments(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
t.Helper() t.Helper()
placement, err := js.KeyValue(ctx, index.PlacementBucket) assignments, err := js.KeyValue(ctx, index.AssignmentsBucket)
if err != nil { if err != nil {
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil { if assignments, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.AssignmentsBucket, History: index.AssignmentsHistory}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@@ -1124,11 +1124,11 @@ func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetS
} }
em := views.Emitter{Owner: index.AssignerModule, Subject: process.Account{Node: "n1", Module: index.AssignerModule}.EventSubject, em := views.Emitter{Owner: index.AssignerModule, Subject: process.Account{Node: "n1", Module: index.AssignerModule}.EventSubject,
Publish: views.JetStreamPublish(js), Marks: marks} Publish: views.JetStreamPublish(js), Marks: marks}
if err := em.Start(ctx, index.PlacementState, placement, nil); err != nil { if err := em.Start(ctx, index.AssignmentsState, assignments, nil); err != nil {
t.Fatal(err) t.Fatal(err)
} }
late := assignerLate late := assignerLate
sub, err := nc.Subscribe("mesh.mod.mesh-delivery-assigner.call.placement.>", func(m *nats.Msg) { sub, err := nc.Subscribe("mesh.mod.mesh-delivery-assigner.call.assignments.>", func(m *nats.Msg) {
if late != nil { // its runtime comes up after the indexer started (DLV-F5r) if late != nil { // its runtime comes up after the indexer started (DLV-F5r)
select { select {
case <-late: case <-late:
@@ -1136,7 +1136,7 @@ func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetS
return return
} }
} }
a, err := views.FirstView(index.AssignerModule, index.PlacementState, jsSource{ctx, placement}, jsSource{ctx, marks}, "", time.Now()) a, err := views.FirstView(index.AssignerModule, index.AssignmentsState, jsSource{ctx, assignments}, jsSource{ctx, marks}, "", time.Now())
out := map[string]any{"result": a, "node": "n0"} out := map[string]any{"result": a, "node": "n0"}
if err != nil { if err != nil {
out = map[string]any{"error": err.Error()} out = map[string]any{"error": err.Error()}
@@ -472,10 +472,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
} }
} }
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the // 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(sortedModules(), ix.PutToolchain, logger.Printf) gate := buildrun.NewToolchainGate(sortedModules(), ix.PutToolchain, logger.Printf)
// What each counted node's engine reads (ADR 0624 §1), for the mesh-wide hold of a manifest every node reads (ADR 0632 // What each counted node's engine reads (ADR 0624 §1), for the mesh-wide hold of a manifest every node reads (ADR 0632
// §2): asked through the node-engine seat's verb as the indexer's own account, for every node the placement names, // §2): asked through the node-engine seat's verb as the indexer's own account, for every node the assignments names,
// kept in the state engine-reads once it is bound; each change weighs the holds again. // kept in the state engine-reads once it is bound; each change weighs the holds again.
reads := newEngineReads(calls.CallOn, gate, func() { ix.KickHolds(ctx) }, logger.Printf) reads := newEngineReads(calls.CallOn, gate, func() { ix.KickHolds(ctx) }, logger.Printf)
gate.OnPlaced = reads.placed gate.OnPlaced = reads.placed
@@ -506,14 +506,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
process.AwaitBucket(ctx, js, conn, index.EngineReadsBucket, "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) { process.AwaitBucket(ctx, js, conn, index.EngineReadsBucket, "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) {
reads.bind(ctx, kv, errs, logger.Printf) reads.bind(ctx, kv, errs, logger.Printf)
}) })
// The placement is the assigner's: read through its first view and its views family, never its bucket (DLV-F1r). // The assignments is the assigner's: read through its first view and its views family, never its bucket (DLV-F1r).
placementErrs := make(chan error) assignmentsErrs := make(chan error)
// The indexer's one pull of its durable on the shared events stream (process.EventHub): every handler is registered // The indexer's one pull of its durable on the shared events stream (process.EventHub): every handler is registered
// before it starts. // before it starts.
hub := process.NewEventHub(logger.Printf) hub := process.NewEventHub(logger.Printf)
followPlacement(ctx, hub, cred, calls.Call, gate, func(ctx context.Context) { followAssignments(ctx, hub, cred, calls.Call, gate, func(ctx context.Context) {
if err := ix.Recompute(ctx); err != nil { if err := ix.Recompute(ctx); err != nil {
logger.Printf("writing the modules again after the placement of a build module changed: %v", err) logger.Printf("writing the modules again after the assignments of a build module changed: %v", err)
} }
}, findings, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "indexer_first_views reads nothing and indexer_first_view_again is not heard", logger.Printf), logger.Printf) }, findings, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "indexer_first_views reads nothing and indexer_first_view_again is not heard", logger.Printf), logger.Printf)
tools := newToolchainWatch(ctx, js, toolchainSink{ tools := newToolchainWatch(ctx, js, toolchainSink{
@@ -649,7 +649,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err := hub.Handle(numberedSubject, versions); err != nil { if err := hub.Handle(numberedSubject, versions); err != nil {
return err return err
} }
_ = notHeard.Bind(process.EventsStream, "no package's series moves, no node's manifest-reads-changed and no placement view is heard until the indexer starts again", func() error { _ = notHeard.Bind(process.EventsStream, "no package's series moves, no node's manifest-reads-changed and no assignments view is heard until the indexer starts again", func() error {
if err := hub.Start(ctx, js, cred); err != nil { if err := hub.Start(ctx, js, cred); err != nil {
stopped = cmp.Or(stopped, err) stopped = cmp.Or(stopped, err)
return err return err
@@ -716,7 +716,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err return err
case err := <-sourceErrs: case err := <-sourceErrs:
return err return err
case err := <-placementErrs: case err := <-assignmentsErrs:
return err return err
case err := <-tools.errs: case err := <-tools.errs:
return err return err
@@ -9,23 +9,23 @@ import (
"git.novox.be/novox/mesh-sdk/go/process" "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-contracts/go/clients/nodeengineclient/enginereads"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"mesh-delivery-indexer/internal/index" "mesh-delivery-indexer/internal/index"
) )
// engineReads is what each counted node's engine states it reads, for the indexer's mesh-wide hold (index.Holds; novox/hq // engineReads is what each counted node's engine states it reads, for the indexer's mesh-wide hold (index.Holds; novox/hq
// ADR 0632 §2, ADR 0624 §1), in the shape the builder keeps it: asked through the node-engine seat's verb manifest-reads // ADR 0632 §2, ADR 0624 §1), in the shape the builder keeps it: asked through the node-engine seat's verb manifest-reads
// for every node the placement names, on the indexer's start, on the seat's event manifest-reads-changed for that node and // for every node the assignments names, on the indexer's start, on the seat's event manifest-reads-changed for that node and
// when a person asks again (indexer_engine_reads, indexer_holds); kept in the indexer's state engine-reads, one key per // when a person asks again (indexer_engine_reads, indexer_holds); kept in the indexer's state engine-reads, one key per
// node, so a node that is away keeps its last statement. A node the placement no longer names is forgotten. Until the // node, so a node that is away keeps its last statement. A node the assignments no longer names is forgotten. Until the
// placement is read no node is counted, and every hold stays. // assignments is read no node is counted, and every hold stays.
type engineReads struct { type engineReads struct {
keeper *enginereads.Keeper keeper *enginereads.Keeper
gate *buildrun.ToolchainGate gate *buildrun.ToolchainGate
mu sync.Mutex mu sync.Mutex
known bool // the placement is read known bool // the assignments is read
nodes []string nodes []string
} }
@@ -54,7 +54,7 @@ func (r *engineReads) bind(ctx context.Context, kv jetstream.KeyValue, errs chan
r.ask(nodes, "the state engine-reads is bound") r.ask(nodes, "the state engine-reads is bound")
} }
// placed takes the placement in, once it is read and after each change of it: a node it names newly is asked, one it no // placed takes the assignments in, once it is read and after each change of it: a node it names newly is asked, one it no
// longer names is forgotten, key and all. // longer names is forgotten, key and all.
func (r *engineReads) placed() { func (r *engineReads) placed() {
nodes := r.gate.Nodes() nodes := r.gate.Nodes()
@@ -64,7 +64,7 @@ func (r *engineReads) placed() {
r.mu.Unlock() r.mu.Unlock()
for _, node := range nodes { for _, node := range nodes {
if first || !slices.Contains(before, node) { 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 { for _, node := range before {
@@ -94,7 +94,7 @@ func (r *engineReads) heard(m jetstream.Msg, logf func(string, ...any)) bool {
} }
// counted is index.Indexer.Counted: what each counted node states (nil, the baseline, for one with no statement), and // counted is index.Indexer.Counted: what each counted node states (nil, the baseline, for one with no statement), and
// whether the placement is read. // whether the assignments is read.
func (r *engineReads) counted() (map[string][]string, bool) { func (r *engineReads) counted() (map[string][]string, bool) {
r.mu.Lock() r.mu.Lock()
nodes, known := slices.Clone(r.nodes), r.known nodes, known := slices.Clone(r.nodes), r.known
@@ -13,15 +13,15 @@ import (
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings" sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-indexer/internal/index" "mesh-delivery-indexer/internal/index"
) )
// findingsBucket is the indexer's own findings, which the condition keeper raises and clears (ADR 0564 §2): a value too // findingsBucket is the indexer's own findings, which the condition keeper raises and clears (ADR 0564 §2): a value too
// large for its views event, and its first view of the assigner's placement unanswered past its bound. // large for its views event, and its first view of the assigner's assignments unanswered past its bound.
var findingsBucket = mcc.FindingsBucket(index.Module) var findingsBucket = mcc.FindingsBucket(index.Module)
// readyOf binds a state's bucket: now for one bound at the start, once it appears for one awaited. // readyOf binds a state's bucket: now for one bound at the start, once it appears for one awaited.
@@ -109,15 +109,15 @@ const graphState = graph.State
// where indexer_first_view_again asks. // where indexer_first_view_again asks.
var firstViewsBucket = index.Module + "_" + views.StatusState var firstViewsBucket = index.Module + "_" + views.StatusState
// placementTool is the assigner's first-view tool of its placement, as the indexer's invokes names it (ADR 0621 §2). // assignmentsTool is the assigner's first-view tool of its assignments, as the indexer's invokes names it (ADR 0621 §2).
const placementTool = index.AssignerModule + "." + index.PlacementState const assignmentsTool = index.AssignerModule + "." + index.AssignmentsState
// followPlacement reads the seat mesh-placement's placement through the assigner's first view and then its views // followAssignments reads the seat mesh-module-assigner's assignments through the assigner's first view and then its views
// family, never its bucket (ADR 0615 §1, DLV-F1r), into gate: each key as it changes, settled once answered, with // family, never its bucket (ADR 0615 §1, DLV-F1r), into gate: each key as it changes, settled once answered, with
// changed told whenever what the gate passes on changed. It waits for the first answer at most views.Bound, so a // changed told whenever what the gate passes on changed. It waits for the first answer at most views.Bound, so a
// normal start, the two owners coming up together, computes the fingerprints once; past it the indexer goes on, the gate // normal start, the two owners coming up together, computes the fingerprints once; past it the indexer goes on, the gate
// passing every report, and raises its condition naming the assigner and placement until the answer clears it. // passing every report, and raises its condition naming the assigner and assignments until the answer clears it.
func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Account, call func(string, any) (json.RawMessage, error), func followAssignments(ctx context.Context, hub *process.EventHub, cred process.Account, call func(string, any) (json.RawMessage, error),
gate *buildrun.ToolchainGate, changed func(context.Context), findings, status *process.AwaitedBucket, logf func(string, ...any)) *views.Follower { gate *buildrun.ToolchainGate, changed func(context.Context), findings, status *process.AwaitedBucket, logf func(string, ...any)) *views.Follower {
finding := func(s views.Status, raised bool) { finding := func(s views.Status, raised bool) {
w := sdkfindings.Writer(ctx, findings.KV) w := sdkfindings.Writer(ctx, findings.KV)
@@ -133,11 +133,11 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
} }
} }
var settled atomic.Bool // the first view answered: the gate's nodes are the counted ones from now on var settled atomic.Bool // the first view answered: the gate's nodes are the counted ones from now on
f := &views.Follower{Owner: index.AssignerModule, Tool: placementTool, State: index.PlacementState, f := &views.Follower{Owner: index.AssignerModule, Tool: assignmentsTool, State: index.AssignmentsState,
Ask: views.AskVia(call, placementTool), Logf: logf, Ask: views.AskVia(call, assignmentsTool), Logf: logf,
Raise: func(s views.Status) { finding(s, true) }, Clear: func(s views.Status) { finding(s, false) }, Raise: func(s views.Status) { finding(s, true) }, Clear: func(s views.Status) { finding(s, false) },
Changed: func(key string, value json.RawMessage, deleted bool, at time.Time) { Changed: func(key string, value json.RawMessage, deleted bool, at time.Time) {
if gate.Placement(key, value, deleted, at) { if gate.Assignments(key, value, deleted, at) {
changed(ctx) changed(ctx)
} }
if settled.Load() && gate.OnPlaced != nil { if settled.Load() && gate.OnPlaced != nil {
@@ -160,21 +160,21 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
} }
settled.Store(true) settled.Store(true)
if gate.OnPlaced != nil { if gate.OnPlaced != nil {
gate.OnPlaced() // the placement is read: its nodes are the counted ones gate.OnPlaced() // the assignments is read: its nodes are the counted ones
} }
close(answered) close(answered)
}() }()
// The assigner's views family, on the module's one hub (the host starts it once every handler is registered); a family the // The assigner's views family, on the module's one hub (the host starts it once every handler is registered); a family the
// hub cannot register is the finding family-not-heard until the indexer starts again (ADR 0615 §5). // hub cannot register is the finding family-not-heard until the indexer starts again (ADR 0615 §5).
_ = notHeardWriter(ctx, findings, logf).Bind(process.EventsStream, "the placement is the first view's alone, asked again by hand, until the indexer starts again", func() error { _ = notHeardWriter(ctx, findings, logf).Bind(process.EventsStream, "the assignments is the first view's alone, asked again by hand, until the indexer starts again", func() error {
return hub.Handle("mesh.mod."+index.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{index.PlacementState: f}, logf)) return hub.Handle("mesh.mod."+index.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{index.AssignmentsState: f}, logf))
}) })
wait, cancel := context.WithTimeout(ctx, views.Bound) // a context's bound, no timer (ADR 0437 §8) wait, cancel := context.WithTimeout(ctx, views.Bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel() defer cancel()
select { select {
case <-answered: case <-answered:
case <-wait.Done(): case <-wait.Done():
logf("the assigner has not answered placement within %s: the indexer goes on, every node's toolchain report counting until it answers", views.Bound) logf("the assigner has not answered assignments within %s: the indexer goes on, every node's toolchain report counting until it answers", views.Bound)
} }
return f return f
} }
+2 -2
View File
@@ -5,8 +5,8 @@ go 1.26.0
require ( require (
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
@@ -129,7 +129,7 @@ type Indexer struct {
// capability read mesh-wide, waiting for every counted node-engine to state it. Nil, or Counted nil, holds nothing. // capability read mesh-wide, waiting for every counted node-engine to state it. Nil, or Counted nil, holds nothing.
Holds Store Holds Store
// Counted is what each counted node's engine states it reads (ADR 0624 §1; nil, the baseline, for a node with no // Counted is what each counted node's engine states it reads (ADR 0624 §1; nil, the baseline, for a node with no
// statement), and whether the counted nodes are known (the placement is read). // statement), and whether the counted nodes are known (the assignments is read).
Counted func() (map[string][]string, bool) Counted func() (map[string][]string, bool)
// AskEngines asks these nodes' engines what they read again (the node-engine seat's verb manifest-reads); nil asks none. // AskEngines asks these nodes' engines what they read again (the node-engine seat's verb manifest-reads); nil asks none.
AskEngines func(nodes []string, why string) AskEngines func(nodes []string, why string)
@@ -28,10 +28,10 @@ import (
// it is written as its merge would have written it, judged and said. The assigner's hold of ADR 0614 §4 holds a want on // it is written as its merge would have written it, judged and said. The assigner's hold of ADR 0614 §4 holds a want on
// one node; this one holds the entry itself, which every node reads. // one node; this one holds the entry itself, which every node reads.
// //
// "Counted" is ADR 0624 §1's set: every node the placement names, present or away, each read as its engine last stated // "Counted" is ADR 0624 §1's set: every node the assignments names, present or away, each read as its engine last stated
// through the node-engine seat's verb manifest-reads (a node with no statement read as the baseline; the indexer's state // through the node-engine seat's verb manifest-reads (a node with no statement read as the baseline; the indexer's state
// engine-reads, nodeengineclient/enginereads). Until the placement is read no node is counted and every hold stays. The hold moves // engine-reads, nodeengineclient/enginereads). Until the assignments is read no node is counted and every hold stays. The hold moves
// on the event manifest-reads-changed, the placement's change, a person's ask again, and a merge; never on a timer. // on the event manifest-reads-changed, the assignments's change, a person's ask again, and a merge; never on a timer.
const ( const (
// HoldsState is the indexer's state of held entries: the bucket mesh-delivery-indexer_holds, keyed as the modules state. // HoldsState is the indexer's state of held entries: the bucket mesh-delivery-indexer_holds, keyed as the modules state.
HoldsState = "holds" HoldsState = "holds"
@@ -126,7 +126,7 @@ func meshWide(needs []string) []string {
} }
// noneCounted is why a hold stays while no node is counted. // noneCounted is why a hold stays while no node is counted.
const noneCounted = "no node is counted yet (the placement is not read), so the hold stays until it is" const noneCounted = "no node is counted yet (the assignments is not read), so the hold stays until it is"
// lacking is, of the counted nodes, each whose engine lacks some of needs, with what it lacks; or why no node is counted. // lacking is, of the counted nodes, each whose engine lacks some of needs, with what it lacks; or why no node is counted.
func (ix *Indexer) lacking(needs []string) (map[string][]string, string) { func (ix *Indexer) lacking(needs []string) (map[string][]string, string) {
@@ -232,7 +232,7 @@ func (ix *Indexer) dropHold(ctx context.Context, key, why string) error {
} }
// KickHolds moves every hold whose needs every counted node-engine now states, in the background, once at a time: on a // KickHolds moves every hold whose needs every counted node-engine now states, in the background, once at a time: on a
// node's statement changed, the placement's change and the holds state's binding. // node's statement changed, the assignments's change and the holds state's binding.
func (ix *Indexer) KickHolds(ctx context.Context) { func (ix *Indexer) KickHolds(ctx context.Context) {
if ix.Holds == nil || ix.Counted == nil { if ix.Holds == nil || ix.Counted == nil {
return return
@@ -37,9 +37,9 @@ const (
CheckerModule = "mesh-delivery-checker" CheckerModule = "mesh-delivery-checker"
BuildGoModule = "mesh-build-go" BuildGoModule = "mesh-build-go"
BuildNodeModule = "mesh-build-node" BuildNodeModule = "mesh-build-node"
PlacementState = "placement" AssignmentsState = "assignments"
PlacementBucket = AssignerModule + "_" + PlacementState AssignmentsBucket = AssignerModule + "_" + AssignmentsState
PlacementHistory = 5 AssignmentsHistory = 5
SourceBucket = GateModule + "_source" SourceBucket = GateModule + "_source"
PreviewsBucket = CheckerModule + "_previews" PreviewsBucket = CheckerModule + "_previews"
PreviewsHistory = 5 PreviewsHistory = 5
@@ -17,7 +17,7 @@ import (
var OldSections = []string{"state", "logs", "streams", "objects"} var OldSections = []string{"state", "logs", "streams", "objects"}
// Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own // Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own
// composition, placement, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the // composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the
// grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new // grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new
// state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused. // state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused.
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
@@ -106,7 +106,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
if m.Module != "mesh-delivery-indexer" { if m.Module != "mesh-delivery-indexer" {
t.Errorf("module = %q", m.Module) t.Errorf("module = %q", m.Module)
} }
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says // 1126: assignments is the seat mesh-module-assigner's (ADR 0567): the indexer holds no assignments state. ADR 0578 §1: it says
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as // a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
// manifest-accepted, both events of the seat mesh-catalog it holds (ADR 0636 §4), so not among its `emits`. // manifest-accepted, both events of the seat mesh-catalog it holds (ADR 0636 §4), so not among its `emits`.
if own := slices.DeleteFunc(slices.Clone(m.Emits), func(e string) bool { return strings.HasPrefix(e, views.EventPrefix) }); !reflect.DeepEqual(own, []string{takein.TakeInPendingEvent, takein.TakeInTakenEvent}) { if own := slices.DeleteFunc(slices.Clone(m.Emits), func(e string) bool { return strings.HasPrefix(e, views.EventPrefix) }); !reflect.DeepEqual(own, []string{takein.TakeInPendingEvent, takein.TakeInTakenEvent}) {
@@ -136,14 +136,14 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
t.Errorf("the indexer reads the bucket %s, its manifest's reads are %v (want %s)", bucket, m.Reads, read) t.Errorf("the indexer reads the bucket %s, its manifest's reads are %v (want %s)", bucket, m.Reads, read)
} }
} }
// DLV-F1r (ADR 0615 §1): the assigner's placement is read through its first view and views family, never its state. // DLV-F1r (ADR 0615 §1): the assigner's assignments is read through its first view and views family, never its state.
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want) t.Errorf("reads = %v, want %v", m.Reads, want)
} }
// "Reads FORGE" is a consumes of the indexer's stream: the broker grants only buckets by reads (mesh-control // "Reads FORGE" is a consumes of the indexer's stream: the broker grants only buckets by reads (mesh-control
// internal/broker/state.go:62-93). // internal/broker/state.go:62-93).
// The registry seats' published and the builder's numbered fill each package's series (DLV-I5, design 51 *The versions*). // The registry seats' published and the builder's numbered fill each package's series (DLV-I5, design 51 *The versions*).
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.placement", if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-builder.version.numbered", "mesh-delivery-checker.preview.written", "mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.started", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**", "mesh-delivery-builder.version.numbered", "mesh-delivery-checker.preview.written", "mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.started", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**",
"node-engine.manifest-reads-changed", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { // ADR 0632 §2: the counted engines' reads "node-engine.manifest-reads-changed", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { // ADR 0632 §2: the counted engines' reads
t.Errorf("consumes = %v, want %v", m.Consumes, want) t.Errorf("consumes = %v, want %v", m.Consumes, want)
@@ -157,7 +157,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths: // Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a // ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
// commit is in another (issue 1900). // commit is in another (issue 1900).
if want := []string{"mesh-delivery-assigner.placement", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) { if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want) t.Errorf("invokes = %v, want %v", m.Invokes, want)
} }
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
+2 -2
View File
@@ -60,7 +60,7 @@
"go-package-registry.published", "go-package-registry.published",
"mesh-build-go.toolchain.reported", "mesh-build-go.toolchain.reported",
"mesh-build-node.toolchain.reported", "mesh-build-node.toolchain.reported",
"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-builder.version.numbered", "mesh-delivery-builder.version.numbered",
"mesh-delivery-checker.preview.written", "mesh-delivery-checker.preview.written",
"mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.ended",
@@ -119,7 +119,7 @@
"mesh-delivery-gate.source" "mesh-delivery-gate.source"
], ],
"invokes": [ "invokes": [
"mesh-delivery-assigner.placement", "mesh-delivery-assigner.assignments",
"gitea.gitea_commit_trees", "gitea.gitea_commit_trees",
"gitea.gitea_contains", "gitea.gitea_contains",
"gitea.gitea_get_file", "gitea.gitea_get_file",
+2 -2
View File
@@ -40,8 +40,8 @@ require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
@@ -2,7 +2,7 @@
// how a module is spoken to"). An ask the teller holds open at the router keeps one of the three places the router gives // how a module is spoken to"). An ask the teller holds open at the router keeps one of the three places the router gives
// its asker (asks.MostOpen) until it is answered, it lapses, or the gate or the assigner takes its key away. When that last // its asker (asks.MostOpen) until it is answered, it lapses, or the gate or the assigner takes its key away. When that last
// never comes (on 2026-10-31 two merge asks of pull requests merged by another path held two places for a day, and a // never comes (on 2026-10-31 two merge asks of pull requests merged by another path held two places for a day, and a
// placement ask was refused for want of room), a person ends the ask by hand. The teller's process serves no request // assignments ask was refused for want of room), a person ends the ask by hand. The teller's process serves no request
// subject (ADR 0437 §4), so the tool writes an end into the teller's state ends, which the teller watches, carries out // subject (ADR 0437 §4), so the tool writes an end into the teller's state ends, which the teller watches, carries out
// and answers there, as the indexer answers indexer_take_in. // and answers there, as the indexer answers indexer_take_in.
package ends package ends
@@ -4,20 +4,22 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"maps"
"mesh-delivery-view/internal/view" "mesh-delivery-view/internal/view"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
@@ -27,11 +29,13 @@ import (
func serve(t *testing.T) string { func serve(t *testing.T) string {
t.Helper() t.Helper()
m := view.New() m := view.New()
placed, err := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}}) placed, err := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
m.Apply(view.Entry{Bucket: view.PlacementBucket, Key: "placement.a", Value: placed}) for _, k := range slices.Sorted(maps.Keys(placed)) { // a key per module, then the node key
m.Apply(view.Entry{Bucket: view.AssignmentsBucket, Key: k, Value: placed[k]})
}
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c") dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -13,9 +13,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
@@ -6,23 +6,25 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"maps"
"mesh-delivery-view/internal/view" "mesh-delivery-view/internal/view"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
@@ -70,7 +72,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
kvs := map[string]jetstream.KeyValue{} kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} { for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}) kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -90,20 +92,22 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
} }
return b return b
} }
// The seat's placement (issue 774, ADR 0549 §3): x and y on a, nothing on b. // The seat's assignments (issue 774, ADR 0549 §3): x and y on a, nothing on b.
placeValue := func(node string, modules ...string) []byte { place := func(node string, modules ...string) {
p := meshmoduleassigner.Placement{Node: node} p := meshmoduleassigner.Assignments{Node: node}
for _, m := range modules { for _, m := range modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m}) p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
} }
v, err := meshmoduleassignerclient.EncodePlacement(p) values, err := meshmoduleassignerclient.AssignmentsValues(p)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return v for _, k := range slices.Sorted(maps.Keys(values)) { // a key per module, then the node key
put(view.AssignmentsBucket, k, values[k])
}
} }
put(view.PlacementBucket, "placement.a", placeValue("a", "x", "y")) place("a", "x", "y")
put(view.PlacementBucket, "placement.b", placeValue("b")) place("b")
put(view.HaveBucket, "a.x", haveValue("b1", nodeengineclient.PhaseChecking, nodeengineclient.HealthWaits)) put(view.HaveBucket, "a.x", haveValue("b1", nodeengineclient.PhaseChecking, nodeengineclient.HealthWaits))
put(view.HaveBucket, "b.x", haveValue("b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK)) put(view.HaveBucket, "b.x", haveValue("b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK))
@@ -182,7 +186,7 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
kvs := map[string]jetstream.KeyValue{} kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} { for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}) kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -269,7 +273,7 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} { for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1,4 +1,4 @@
// Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, the seat's placement // Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, the seat's assignments
// and builds buckets on its own bus credential, derives one row per module and node in memory, and serves them on a // and builds buckets on its own bus credential, derives one row per module and node in memory, and serves them on a
// local socket (for mesh-delivery-cli) and on a local web page. It starts, stops or retries nothing; the one thing it writes is the operator's answer to an ask that needs no proof of who // local socket (for mesh-delivery-cli) and on a local web page. It starts, stops or retries nothing; the one thing it writes is the operator's answer to an ask that needs no proof of who
// answered, under `<ask key>.view` in its bucket answers (ADR 0528 clause 6). // answered, under `<ask key>.view` in its bucket answers (ADR 0528 clause 6).
@@ -21,9 +21,9 @@ import (
"syscall" "syscall"
"time" "time"
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
"git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views"
@@ -126,7 +126,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
} }
} }
open := buckets{js: js, reader: reader} open := buckets{js: js, reader: reader}
for _, bucket := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} { for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
go follow(ctx, open, bucket, false, model, logger) go follow(ctx, open, bucket, false, model, logger)
} }
// The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556). // The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556).
@@ -185,7 +185,7 @@ var (
// owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the // owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the
// view's tools ask the other six through the owners' read tools on each call. // view's tools ask the other six through the owners' read tools on each call.
var ownersFollowed = []struct{ owner, state, tool string }{ var ownersFollowed = []struct{ owner, state, tool string }{
{view.AssignerModule, view.PlacementState, view.AssignerModule + ".assigner_read_placement"}, {view.AssignerModule, view.AssignmentsState, view.AssignerModule + ".assigner_read_assignments"},
{view.AssignerModule, "wants", view.AssignerModule + ".assigner_read_wants"}, {view.AssignerModule, "wants", view.AssignerModule + ".assigner_read_wants"},
{view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"}, {view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"},
{view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"}, {view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"},
+4 -4
View File
@@ -7,9 +7,9 @@ require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
@@ -58,7 +58,7 @@ require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
@@ -11,8 +11,8 @@ import (
"time" "time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
) )
@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
) )
@@ -2,7 +2,7 @@ package view
import ( import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-sdk/go/motion" "git.novox.be/novox/mesh-sdk/go/motion"
) )
@@ -35,8 +35,8 @@ const (
IndexerConfigCandidatesState = "config-candidates" IndexerConfigCandidatesState = "config-candidates"
// The assigner's states. // The assigner's states.
PlacementState = "placement" AssignmentsState = "assignments"
PlacementBucket = AssignerModule + "_" + PlacementState AssignmentsBucket = AssignerModule + "_" + AssignmentsState
AssignerWantsBucket = AssignerModule + "_wants" AssignerWantsBucket = AssignerModule + "_wants"
AssignerAsksBucket = AssignerModule + "_asks" AssignerAsksBucket = AssignerModule + "_asks"
@@ -7,9 +7,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"

Some files were not shown because too many files have changed in this diff Show More