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:
@@ -5,7 +5,7 @@
|
||||
// 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
|
||||
// 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
|
||||
|
||||
import (
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"fmt"
|
||||
"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/placement/go/clients/meshmoduleassignerclient"
|
||||
"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/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
|
||||
// composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14).
|
||||
AskKindRecord = "record"
|
||||
// AskKindPlacement is the kind of the authorising ask about a proposal of placement (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
|
||||
// AskKindAssignments is the kind of the authorising ask about a proposal of assignments (novox/hq ADR 0567 §5):
|
||||
// `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).
|
||||
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
|
||||
// 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 {
|
||||
Proposal string
|
||||
Kind string
|
||||
@@ -60,11 +60,11 @@ func AskNeedsProof(kind string) (bool, error) {
|
||||
switch kind {
|
||||
case meshdeliveryclient.AskKindMerge:
|
||||
return true, nil
|
||||
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement:
|
||||
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments:
|
||||
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,
|
||||
AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement)
|
||||
AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// PlacementAskKey is the key of the authorising ask about a proposal of placement: `placement.<proposal id>` (ADR 0567 §5).
|
||||
func PlacementAskKey(proposal string) (string, error) {
|
||||
// AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.<proposal id>` (ADR 0567 §5).
|
||||
func AssignmentsAskKey(proposal string) (string, error) {
|
||||
if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil {
|
||||
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:
|
||||
@@ -124,7 +124,7 @@ func RecordAskKey(node, module, composed string) (string, error) {
|
||||
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.
|
||||
func ParseAnyAskKey(key string) (AskKey, error) {
|
||||
kind, rest, _ := strings.Cut(key, ".")
|
||||
@@ -167,7 +167,7 @@ func ParseAnyAskKey(key string) (AskKey, error) {
|
||||
return refused(err)
|
||||
}
|
||||
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
|
||||
case AskKindPlacement:
|
||||
case AskKindAssignments:
|
||||
if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil {
|
||||
return refused(err)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) {
|
||||
hold, _ := HoldAskKey("gitea", fp)
|
||||
restore, _ := RestoreAskKey("n1", "gitea", fp)
|
||||
backup, _ := BackupAskKey("n1", "novox.be", fp)
|
||||
placement, _ := PlacementAskKey("p-20261028t183000-1a2b3c")
|
||||
assignments, _ := AssignmentsAskKey("p-20261028t183000-1a2b3c")
|
||||
record, _ := RecordAskKey("n1", "gitea", digest)
|
||||
for key, want := range map[string]struct {
|
||||
got string
|
||||
back AskKey
|
||||
}{
|
||||
"hold": {hold, AskKey{Kind: "hold", 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}},
|
||||
"placement": {placement, AskKey{Kind: "placement", Proposal: "p-20261028t183000-1a2b3c"}},
|
||||
"record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}},
|
||||
"hold": {hold, AskKey{Kind: "hold", 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}},
|
||||
"assignments": {assignments, AskKey{Kind: "assignments", Proposal: "p-20261028t183000-1a2b3c"}},
|
||||
"record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}},
|
||||
} {
|
||||
back, err := ParseAnyAskKey(want.got)
|
||||
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,
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
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/meshdeliveryclient 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 (
|
||||
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/seats/containerimageregistry 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"
|
||||
"io"
|
||||
"maps"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
mdcc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"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/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -37,10 +37,10 @@ import (
|
||||
|
||||
"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"
|
||||
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"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
@@ -56,7 +56,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"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
|
||||
@@ -67,7 +67,7 @@ type bed struct {
|
||||
ctx context.Context
|
||||
dir string
|
||||
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
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ func saysWhatItKeeps(t *testing.T, b *bed, done <-chan error) {
|
||||
func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) {
|
||||
for _, missing := range readBuckets {
|
||||
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)
|
||||
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.
|
||||
pb := startBed(t, plc.PlacementBucket)
|
||||
pb := startBed(t, plc.AssignmentsBucket)
|
||||
pdone := pb.start(t)
|
||||
select {
|
||||
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):
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
// (research 072 row 37). The placement alone names where modules run (ADR 0567 §8).
|
||||
// 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 assignments alone names where modules run (ADR 0567 §8).
|
||||
func (b *bed) place(t *testing.T, node, module string) {
|
||||
t.Helper()
|
||||
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) {
|
||||
b.placed[node] = append(b.placed[node], module)
|
||||
}
|
||||
pl := mma.Placement{Node: node}
|
||||
pl := mma.Assignments{Node: node}
|
||||
for _, m := range b.placed[node] {
|
||||
pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m})
|
||||
}
|
||||
values, err := mmac.PlacementValues(pl)
|
||||
values, err := mmac.AssignmentsValues(pl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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
|
||||
// its mark, and refuses every other account, the assigner's own included.
|
||||
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"},
|
||||
"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 {
|
||||
e, err := b.buckets[assign.FindingsBucket].Get(b.ctx, graphKey)
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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"} {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkey, _ := mmac.LegacyKey(node)
|
||||
b.putJSON(t, plc.PlacementBucket, pkey, pl)
|
||||
for _, k := range slices.Sorted(maps.Keys(values)) {
|
||||
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 {
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// 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"}]`
|
||||
@@ -1095,7 +1106,7 @@ func (s bedStore) kv(state string) jetstream.KeyValue {
|
||||
if strings.Contains(state, ".") {
|
||||
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) {
|
||||
@@ -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
|
||||
// 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,
|
||||
// and declined, writes nothing and is withdrawn.
|
||||
func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
b := startBed(t, "")
|
||||
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: "MOD_" + placement.Module + "_placement",
|
||||
Subjects: placementSubjects()}); err != nil { // the seat's four events: the views family has a stream of its own
|
||||
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: "MOD_" + assignment.Module + "_assignments",
|
||||
Subjects: assignmentsSubjects()}); err != nil { // the seat's four events: the views family has a stream of its own
|
||||
t.Fatal(err)
|
||||
}
|
||||
heard := make(chan mma.PlacementEvent, 16)
|
||||
heard := make(chan mma.AssignmentsEvent, 16)
|
||||
nc, err := nats.Connect(b.srv.ClientURL())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
if _, err := nc.Subscribe(seatdef.EventSubject(mma.Seat, mma.EventPlacementChanged), func(m *nats.Msg) {
|
||||
var e mma.PlacementEvent
|
||||
if _, err := nc.Subscribe(seatdef.EventSubject(mma.Seat, mma.EventAssignmentsChanged), func(m *nats.Msg) {
|
||||
var e mma.AssignmentsEvent
|
||||
if json.Unmarshal(m.Data, &e) == nil {
|
||||
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, "shell", []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
|
||||
// answers its node key's revision; legacy writes it in the form before, placement.<node>, alone.
|
||||
place := func(node string, legacy bool, modules ...string) uint64 {
|
||||
p := mma.Placement{Node: node, Modules: []mma.PlacedModule{}}
|
||||
// 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.
|
||||
place := func(node string, modules ...string) uint64 {
|
||||
p := mma.Assignments{Node: node, Modules: []mma.PlacedModule{}}
|
||||
for _, m := range modules {
|
||||
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
|
||||
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)
|
||||
if legacy {
|
||||
key, _ = mmac.LegacyKey(node)
|
||||
values = map[string][]byte{key: values[key]}
|
||||
}
|
||||
for _, k := range slices.Sorted(maps.Keys(values)) {
|
||||
if k == key {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return rev
|
||||
}
|
||||
place("anchor", true, "web")
|
||||
laptopRev := place("laptop", false, "shell")
|
||||
place("anchor", "web")
|
||||
laptopRev := place("laptop", "shell")
|
||||
bound := judge.AnswerBound
|
||||
t.Cleanup(func() { judge.AnswerBound = bound })
|
||||
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.
|
||||
var anchorRev uint64
|
||||
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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1250,33 +1257,29 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
}
|
||||
answerAll(p)
|
||||
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)
|
||||
select {
|
||||
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 ||
|
||||
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):
|
||||
t.Fatal("no placement-changed heard")
|
||||
t.Fatal("no assignments-changed heard")
|
||||
}
|
||||
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{}
|
||||
for _, key := range []string{node + ".node", "placement." + node, node + ".web", node + ".shell"} {
|
||||
if e, err := b.buckets[plc.PlacementBucket].Get(b.ctx, key); err == nil {
|
||||
for _, key := range []string{node + ".node", node + ".web", node + ".shell"} {
|
||||
if e, err := b.buckets[plc.AssignmentsBucket].Get(b.ctx, key); err == nil {
|
||||
values[key] = e.Value()
|
||||
}
|
||||
}
|
||||
got, err := mmac.PlacementsOf(values)
|
||||
got, err := mmac.AssignmentsOf(values)
|
||||
if err != nil || !slices.Equal(mmac.Assigned(got[node]), 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) {
|
||||
t.Error("an applied proposal stays in the state")
|
||||
@@ -1288,13 +1291,13 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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 {
|
||||
return strings.Contains(p.Words, "laid again") && len(p.Must) > 0
|
||||
})
|
||||
answerAll(relaid)
|
||||
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)
|
||||
for deadline := time.Now().Add(15 * time.Second); b.hasKey(plc.ProposalsBucket, moved.ID); time.Sleep(50 * time.Millisecond) {
|
||||
if time.Now().After(deadline) {
|
||||
@@ -1302,7 +1305,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
// 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 len(heard) > 0 {
|
||||
<-heard
|
||||
@@ -1329,7 +1332,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
// now, on a node of its own.
|
||||
hubRecord, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "hub"}})
|
||||
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"}`,
|
||||
"changes": `[{"op":"add-node","node":"ace","decided":{"address":"10.9.0.2"},"keys":{"overlay":"dHVubmVs"}}]`}, "novox.mesh-enrolment")
|
||||
if err != nil {
|
||||
@@ -1338,19 +1341,19 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
select {
|
||||
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" {
|
||||
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):
|
||||
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)
|
||||
}
|
||||
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")
|
||||
}
|
||||
// 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 {
|
||||
<-heard
|
||||
}
|
||||
@@ -1361,12 +1364,12 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
select {
|
||||
case e := <-heard:
|
||||
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):
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1382,8 +1385,8 @@ func (b *bed) viewsHold(t *testing.T, state, key string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
return views.FirstView(placement.Module, state, src(kv), src(marks), "", time.Now())
|
||||
f := &views.Follower{Owner: assignment.Module, Tool: state, State: state, Ask: func(context.Context, string) (views.Answer, error) {
|
||||
return views.FirstView(assignment.Module, state, src(kv), src(marks), "", time.Now())
|
||||
}}
|
||||
if err := f.Start(b.ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1392,7 +1395,7 @@ func (b *bed) viewsHold(t *testing.T, state, key string) {
|
||||
gap := false
|
||||
for seq := info.State.FirstSeq; seq > 0 && seq <= info.State.LastSeq; 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// placementSubjects are the subjects of the seat's four events on the assigner's stream placement.
|
||||
func placementSubjects() []string {
|
||||
// assignmentsSubjects are the subjects of the seat's four events on the assigner's stream assignments.
|
||||
func assignmentsSubjects() []string {
|
||||
var out []string
|
||||
for _, e := range mma.Events() {
|
||||
out = append(out, process.ModuleStreamSubject(placement.Module, e))
|
||||
out = append(out, process.ModuleStreamSubject(assignment.Module, e))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1553,7 +1556,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
|
||||
outR, outW := io.Pipe()
|
||||
t.Cleanup(func() { inW.Close(); outW.Close() })
|
||||
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)
|
||||
go func() {
|
||||
@@ -1574,7 +1577,7 @@ func firstViewsBeforeAnyNewFormState(t *testing.T, b *bed, granted map[string][]
|
||||
t.Fatal(err)
|
||||
}
|
||||
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{}{}
|
||||
defer func() { <-one }()
|
||||
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) {
|
||||
return views.AskVia(func(key string, body any) (json.RawMessage, error) {
|
||||
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 {
|
||||
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)
|
||||
case slices.Contains(allowed, user):
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
raw, _ := json.Marshal(body)
|
||||
var c plc.Change
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"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"
|
||||
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"
|
||||
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/process"
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
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
|
||||
// (ADR 0567 §1), named after the assigner as every module state is.
|
||||
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,
|
||||
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
|
||||
// the verb manifest-reads last answered it or the fallback read it: what assigner_engine_reads lists and asks again.
|
||||
const engineReadsState = "engine-reads"
|
||||
|
||||
// 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
|
||||
// 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}
|
||||
|
||||
// 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 teller, its puts by whoever follows one (ADR 0546), and the seat mesh-module-assigner's placement, proposals and
|
||||
// candidates by the node agents, the grants part, mesh-config and the view (ADR 0567 §9), and its placement by
|
||||
// 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 assignments by
|
||||
// the builder, the checker and the indexer, through mesh-sdk's toolchain gate (issue 774, ADR 0549 §3).
|
||||
var keptStates = []announce.State{
|
||||
{Name: mma.StatePlacement, Readable: true},
|
||||
{Name: mma.StateAssignments, Readable: true},
|
||||
{Name: mma.StateProposals, Readable: true},
|
||||
{Name: mma.StateCandidates, 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.StatusState, Readable: true}, // its first views of the indexer's states (views.go)
|
||||
{Name: engineReadsState, Readable: true},
|
||||
{Name: retiredState}, // read only by the copy into assignments (migrate.go)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
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)
|
||||
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 {
|
||||
// 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)
|
||||
@@ -187,7 +194,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
|
||||
func startsWithout(name string) bool {
|
||||
switch name {
|
||||
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 slices.Contains(seatBuckets, name) || name == buckets.Grants
|
||||
@@ -271,7 +278,7 @@ var watched = []string{
|
||||
buckets.Grants,
|
||||
buckets.Answers,
|
||||
buckets.Builds,
|
||||
plc.PlacementBucket,
|
||||
plc.AssignmentsBucket,
|
||||
plc.ProposalsBucket,
|
||||
plc.CandidatesBucket,
|
||||
plc.PulsesBucket,
|
||||
@@ -419,11 +426,11 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
name string
|
||||
kv jetstream.KeyValue
|
||||
}
|
||||
placementAppeared := make(chan appeared, len(seatBuckets)+2)
|
||||
assignmentsAppeared := make(chan appeared, len(seatBuckets)+2)
|
||||
for _, name := range seatBuckets {
|
||||
if kvs[name] == nil && js != nil {
|
||||
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.
|
||||
@@ -431,16 +438,20 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
if kvs[name] == nil && js != nil {
|
||||
name := name
|
||||
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 {
|
||||
return nil // bound already, by the other of its two events
|
||||
}
|
||||
kvs[a.name] = a.kv
|
||||
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 {
|
||||
select {
|
||||
@@ -476,8 +487,8 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
case err := <-errs:
|
||||
return err
|
||||
case <-settled:
|
||||
case kv := <-placementAppeared:
|
||||
if err := takePlacement(kv); err != nil {
|
||||
case kv := <-assignmentsAppeared:
|
||||
if err := takeAssignments(kv); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-dirty:
|
||||
@@ -491,7 +502,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
continue
|
||||
@@ -511,7 +522,7 @@ var PutsReopenEvery = time.Minute
|
||||
// (ADR 0437 clause 9).
|
||||
func sayVia(ctx context.Context, js jetstream.JetStream) func(oc.Condition) error {
|
||||
return func(c oc.Condition) error {
|
||||
subject, err := occ.ConditionSubject(placement.Module)
|
||||
subject, err := occ.ConditionSubject(assignment.Module)
|
||||
if err != nil {
|
||||
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
|
||||
// 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)) {
|
||||
account := process.Account{Node: thisNode, Module: placement.Module}
|
||||
account := process.Account{Node: thisNode, Module: assignment.Module}
|
||||
w := notHeardWriter(findings, logf)
|
||||
handle := func(m jetstream.Msg) {
|
||||
if err := m.Ack(); err != nil {
|
||||
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
|
||||
"context"
|
||||
"errors"
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
"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"
|
||||
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/process"
|
||||
)
|
||||
@@ -112,12 +112,12 @@ func TestOpensWhatItsManifestNames(t *testing.T) {
|
||||
for _, name := range append(append([]string{}, readBuckets...), ownBuckets...) {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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})
|
||||
}
|
||||
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) Revision() uint64 { return 1 }
|
||||
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"
|
||||
|
||||
"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/modules/mesh-delivery-assigner/asks"
|
||||
"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"
|
||||
|
||||
"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
|
||||
@@ -74,8 +74,8 @@ type passer struct {
|
||||
seat *seatActs
|
||||
publish publisher
|
||||
saidProposed map[string]bool
|
||||
// convertedAll says every node in the form before was converted to its node and module keys (convertPlacement).
|
||||
convertedAll bool
|
||||
// copied says the retired state was copied into `assignments` by this process, or found empty (copyRetired).
|
||||
copied bool
|
||||
// saidWaits is the last said of the seat's wait for its states (sayWaits).
|
||||
saidWaits string
|
||||
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) {
|
||||
p.placementSeat(ctx)
|
||||
p.assignmentsSeat(ctx)
|
||||
p.wants(ctx)
|
||||
p.puts(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
|
||||
// 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() {
|
||||
if p.findings.Put == nil {
|
||||
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
|
||||
// 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.
|
||||
func (p *passer) seatHolders(ctx context.Context) {
|
||||
kv := p.kvs[plc.SeatHoldersBucket]
|
||||
if kv == nil {
|
||||
return
|
||||
}
|
||||
placements := p.w.placements()
|
||||
if len(placements) == 0 {
|
||||
return // no placement read yet: nothing is known of who holds what, and nothing is deleted
|
||||
assignments := p.w.assignments()
|
||||
if len(assignments) == 0 {
|
||||
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)
|
||||
for key, sh := range want {
|
||||
value, err := json.Marshal(sh)
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
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"
|
||||
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"
|
||||
mdseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
@@ -26,10 +26,10 @@ import (
|
||||
|
||||
"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"
|
||||
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/catalogue"
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"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.
|
||||
@@ -70,20 +70,20 @@ func newPasser(kvs kvSet, at time.Time) *passer {
|
||||
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).
|
||||
func placeOn(t *testing.T, w *world, node string, modules ...string) {
|
||||
t.Helper()
|
||||
pl := mma.Placement{Node: node}
|
||||
pl := mma.Assignments{Node: node}
|
||||
for _, m := range modules {
|
||||
pl.Modules = append(pl.Modules, mma.PlacedModule{Module: m})
|
||||
}
|
||||
values, err := mmac.PlacementValues(pl)
|
||||
values, err := mmac.AssignmentsValues(pl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
aWantWaitsOnTheVerbEdgesTheGraphKeeps(t)
|
||||
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
|
||||
// module in an assignment only is no module of its node, and before any placement is read nothing is placed.
|
||||
func aPlacementAloneNamesWhereModulesRun(t *testing.T) {
|
||||
// 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 assignments is read nothing is placed.
|
||||
func aAssignmentsAloneNamesWhereModulesRun(t *testing.T) {
|
||||
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
|
||||
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, "n2", "d")
|
||||
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})
|
||||
put(buckets.Modules, "web", mod) // the trunk's build is not done: rule 1 holds g14 at fpOld
|
||||
placeOn(t, p.w, "g14", "web")
|
||||
pl, _ := mmac.EncodePlacement(mma.Placement{Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
put(plc.PlacementBucket, "placement.g14", pl)
|
||||
pl, _ := mmac.EncodeAssignments(mma.Assignments{Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
put(plc.AssignmentsBucket, "assignments.g14", pl)
|
||||
on, _ := presenceValue(true)
|
||||
put(buckets.Presence, "g14", on)
|
||||
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
|
||||
// placement-changed said, and no verb of the controller is called; the refused-call conditions a previous process raised are
|
||||
// 774/S25/AC1 (ADR 0567 §8): an approved assignments change is written and followed with its warrant recorded and
|
||||
// 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.
|
||||
func TestAnApprovedPlacementCallsNoControllerVerb(t *testing.T) {
|
||||
func TestAnApprovedAssignmentsCallsNoControllerVerb(t *testing.T) {
|
||||
p := newPasser(kvSet{}, time.Date(2026, 11, 2, 9, 0, 0, 0, time.UTC))
|
||||
bus := &fakeBus{calls: make(chan string, 8)}
|
||||
p.seat = newSeatActs(bus, func() {}, func(string, ...any) {})
|
||||
var said []string
|
||||
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"}
|
||||
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})
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
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-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -24,24 +24,24 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"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"
|
||||
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"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
"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
|
||||
// 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
|
||||
// candidate; their answers are judged (placement-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),
|
||||
// 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 (assignments-changed),
|
||||
// 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.
|
||||
|
||||
// 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.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-time", time.Now().UTC().Format(time.RFC3339Nano))
|
||||
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
|
||||
// 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 {
|
||||
bus caller
|
||||
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}}
|
||||
}
|
||||
|
||||
// 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
|
||||
// the controller's while that seat has no holder (ADR 0565 §11, meshhandactsclient.Recorder).
|
||||
func (a *seatActs) record(warrant string) {
|
||||
@@ -92,9 +92,9 @@ func (a *seatActs) record(warrant string) {
|
||||
ask, _, _ := strings.Cut(warrant, "@")
|
||||
go func() {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// seatPlacements is every node's placement and the revision of its node key (its legacy key's for a node not yet
|
||||
// converted), as the watch holds them: the revision a proposal is laid over (§2, research 072 row 37).
|
||||
func (p *passer) seatPlacements() (map[string]mma.Placement, map[string]uint64) {
|
||||
return p.w.placement.AllRevisions()
|
||||
// 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).
|
||||
func (p *passer) seatAssignments() (map[string]mma.Assignments, map[string]uint64) {
|
||||
return p.w.assignmentKeys.AllRevisions()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// placementSeat takes every open proposal one step further.
|
||||
func (p *passer) placementSeat(ctx context.Context) {
|
||||
// assignmentsSeat takes every open proposal one step further.
|
||||
func (p *passer) assignmentsSeat(ctx context.Context) {
|
||||
for _, b := range seatBuckets {
|
||||
if p.kvs[b] == nil {
|
||||
return
|
||||
@@ -204,8 +203,10 @@ func (p *passer) placementSeat(ctx context.Context) {
|
||||
if p.saidProposed == nil {
|
||||
p.saidProposed, p.intended = map[string]bool{}, map[string]map[string]map[string]bool{}
|
||||
}
|
||||
p.convertPlacement(ctx)
|
||||
current, revs := p.seatPlacements()
|
||||
if !p.copyRetired(ctx) {
|
||||
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)
|
||||
raws := p.w.read(plc.ProposalsBucket)
|
||||
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.
|
||||
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 {
|
||||
p.logf("the ask about the proposal %s is not written: %v", prop.ID, err)
|
||||
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.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
if prop.Withdrawn != "" {
|
||||
@@ -281,14 +282,14 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
|
||||
}
|
||||
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.
|
||||
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
|
||||
}
|
||||
if prop.Warrant != nil {
|
||||
p.addNode(ctx, prop, revs)
|
||||
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))
|
||||
if prop.State == mmac.ProposalAsked {
|
||||
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) {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
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)
|
||||
if got, ok := p.intended[key]; ok {
|
||||
return got
|
||||
@@ -381,14 +382,14 @@ func (p *passer) intendedOf(prop mma.Proposal, current map[string]mma.Placement,
|
||||
return got
|
||||
}
|
||||
|
||||
// compute is what the proposal changes on each node by its placement, 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
|
||||
// compute is what the proposal changes on each node by its assignments, and the nodes that answer it: every node with a
|
||||
// assignments or a record (judge.Intended, ADR 0593 §8). The holder composes nothing: each node says what its compositions
|
||||
// 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) {
|
||||
laid := map[string]*mma.Placement{}
|
||||
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.Assignments{}
|
||||
for _, node := range plc.Nodes(prop) {
|
||||
if c, ok := cands[node]; ok && !c.Reached {
|
||||
laid[node] = c.Placement
|
||||
laid[node] = c.Assignments
|
||||
}
|
||||
}
|
||||
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
|
||||
// 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) {
|
||||
if _, ok := cands[node]; !ok {
|
||||
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}
|
||||
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) })
|
||||
}
|
||||
@@ -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
|
||||
// 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)
|
||||
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})
|
||||
@@ -448,7 +449,7 @@ func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, cu
|
||||
p.deletePulses(ctx, prop.ID)
|
||||
for _, node := range plc.Nodes(again) {
|
||||
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) })
|
||||
}
|
||||
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
|
||||
// proposal deleted, and placement-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) {
|
||||
if err := p.performs(akey, assign.PlacementAct(prop)); err != nil {
|
||||
// proposal deleted, and assignments-changed said.
|
||||
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.AssignmentsAct(prop)); err != nil {
|
||||
p.endProposal(ctx, prop, "its warrant does not bind these changes, so nothing is written: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -481,9 +482,9 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
|
||||
return
|
||||
}
|
||||
cands, _, _ := p.candidatesOf(prop.ID)
|
||||
laid := map[string]*mma.Placement{}
|
||||
laid := map[string]*mma.Assignments{}
|
||||
for _, node := range plc.Nodes(prop) {
|
||||
laid[node] = cands[node].Placement
|
||||
laid[node] = cands[node].Assignments
|
||||
}
|
||||
writes, err := assign.Writes(prop, laid)
|
||||
if err != nil {
|
||||
@@ -491,7 +492,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
|
||||
return
|
||||
}
|
||||
var changes []plc.Change
|
||||
writtenLaid := map[string]*mma.Placement{}
|
||||
writtenLaid := map[string]*mma.Assignments{}
|
||||
after := maps.Clone(current)
|
||||
afterRevs := maps.Clone(revs)
|
||||
for _, w := range writes {
|
||||
@@ -499,7 +500,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
|
||||
if err != nil {
|
||||
// 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.
|
||||
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)
|
||||
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")
|
||||
@@ -509,7 +510,7 @@ func (p *passer) warranted(ctx context.Context, prop mma.Proposal, rev uint64, a
|
||||
return
|
||||
}
|
||||
before := current[w.Node]
|
||||
now := mma.Placement{Node: w.Node}
|
||||
now := mma.Assignments{Node: w.Node}
|
||||
if w.Value != nil {
|
||||
now = *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
|
||||
// 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
|
||||
// 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) {
|
||||
current, _ := p.seatPlacements()
|
||||
current, _ := p.seatAssignments()
|
||||
if moved(prop, revs) {
|
||||
p.endProposal(ctx, prop, "a key it was laid over was written since: the enrolment proposes the node again")
|
||||
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())
|
||||
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
|
||||
p.deleteCandidates(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
|
||||
// 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).
|
||||
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
|
||||
before, ok := current[node]
|
||||
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())
|
||||
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
|
||||
p.deleteCandidates(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)
|
||||
w := prop.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})
|
||||
}
|
||||
|
||||
// followWrite follows the keys a warrant wrote: the warrant recorded and placement-changed said. No verb of the
|
||||
// controller is called: the placement is the one source of where modules run (ADR 0567 §8, issue 774).
|
||||
// followWrite follows the keys a warrant wrote: the warrant recorded and assignments-changed said. No verb of the
|
||||
// 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) {
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
@@ -605,7 +606,7 @@ func (p *passer) followWrite(prop mma.Proposal, answer mdc.Answer, changes []plc
|
||||
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))
|
||||
for n := range m {
|
||||
out = append(out, n)
|
||||
@@ -614,7 +615,7 @@ func sortedNodes(m map[string]*mma.Placement) []string {
|
||||
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) {
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
@@ -626,14 +627,14 @@ func (p *passer) sayChanged(prop mma.Proposal, answer mdc.Answer, changes []plc.
|
||||
}
|
||||
}
|
||||
plc.SortChanges(changes)
|
||||
p.sayEvent(mma.EventPlacementChanged, mma.PlacementEvent{Proposal: prop, Nodes: changeBodies(changes),
|
||||
Warrant: &mma.PlacementEventWarrant{Warrant: answer.Warrant, Words: answer.Words, At: at}}, prop.ID+".changed")
|
||||
p.sayEvent(mma.EventAssignmentsChanged, mma.AssignmentsEvent{Proposal: prop, Nodes: changeBodies(changes),
|
||||
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.
|
||||
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 {
|
||||
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.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.
|
||||
@@ -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.
|
||||
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 {
|
||||
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
|
||||
// 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;
|
||||
// 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
|
||||
// reads it.
|
||||
func (p *passer) writeNode(ctx context.Context, node string, after *mma.Placement) (uint64, error) {
|
||||
writes, err := p.w.placement.Writes(node, after)
|
||||
func (p *passer) writeNode(ctx context.Context, node string, after *mma.Assignments) (uint64, error) {
|
||||
writes, err := p.w.assignmentKeys.Writes(node, after)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
kv := p.kvs[plc.PlacementBucket]
|
||||
kv := p.kvs[plc.AssignmentsBucket]
|
||||
committed, rev := false, uint64(0)
|
||||
for _, w := range writes {
|
||||
got, err := writeKey(ctx, kv, w)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
committed, rev = true, got
|
||||
}
|
||||
@@ -726,7 +727,7 @@ func (p *passer) writeNode(ctx context.Context, node string, after *mma.Placemen
|
||||
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) {
|
||||
switch {
|
||||
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 }
|
||||
|
||||
// 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-delivery/contracts/go/clients/meshcatalogclient"
|
||||
|
||||
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"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
|
||||
@@ -59,18 +59,18 @@ func newSeatTools(st seatStore, ask func(string, any) (json.RawMessage, error))
|
||||
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 {
|
||||
Placement mma.Placement `json:"placement"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Assignments mma.Assignments `json:"assignments"`
|
||||
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).
|
||||
func (s *seatTools) placements() (map[string]mma.Placement, map[string]uint64, error) {
|
||||
keys, err := s.st.keys(mma.StatePlacement)
|
||||
func (s *seatTools) assignments() (map[string]mma.Assignments, map[string]uint64, error) {
|
||||
keys, err := s.st.keys(mma.StateAssignments)
|
||||
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)
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
raw, rev, found, err := s.st.get(mma.StatePlacement, key)
|
||||
raw, rev, found, err := s.st.get(mma.StateAssignments, key)
|
||||
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 {
|
||||
continue
|
||||
@@ -92,7 +92,7 @@ func (s *seatTools) placements() (map[string]mma.Placement, map[string]uint64, e
|
||||
}
|
||||
out, revs := held.AllRevisions()
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
// 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())
|
||||
for _, node := range plc.Nodes(p) {
|
||||
key, err := plc.CandidateKey(p.ID, node)
|
||||
if err != nil {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
current, revs, err := s.placements()
|
||||
current, revs, err := s.assignments()
|
||||
if err != nil {
|
||||
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
|
||||
// placement-withdrawn carrying it whole.
|
||||
// assignments-withdrawn carrying it whole.
|
||||
func (s *seatTools) doWithdraw(args map[string]any, by string) (any, error) {
|
||||
why := arg(args, "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
|
||||
}
|
||||
|
||||
// placementAnswer is what the verb placement answers.
|
||||
type placementAnswer struct {
|
||||
// assignmentsAnswer is what the verb assignments answers.
|
||||
type assignmentsAnswer struct {
|
||||
Keys map[string]keyed `json:"keys,omitempty"`
|
||||
Nodes []string `json:"nodes,omitempty"`
|
||||
Count int `json:"count"`
|
||||
@@ -340,18 +340,18 @@ type historyEntry struct {
|
||||
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.
|
||||
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" {
|
||||
return s.seats(arg(args, "node"))
|
||||
}
|
||||
all, revs, err := s.placements()
|
||||
all, revs, err := s.assignments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
if node != "" && n != node {
|
||||
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+" ") {
|
||||
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)
|
||||
}
|
||||
sort.Strings(out.Nodes)
|
||||
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" {
|
||||
return out, nil
|
||||
@@ -402,8 +402,8 @@ func (s *seatTools) doPlacement(args map[string]any) (any, error) {
|
||||
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 "+
|
||||
"the seat's subject on the shared events stream, kept %d days", plc.PlacementHistory, plc.EventDays)
|
||||
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.AssignmentsHistory, plc.EventDays)
|
||||
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.
|
||||
func (s *seatTools) tools() []stdio.Tool {
|
||||
run := map[string]func(map[string]any) (any, error){
|
||||
mma.VerbPlacement: s.doPlacement,
|
||||
mma.VerbProposals: s.doProposals,
|
||||
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.VerbAgain: s.doAgain,
|
||||
mma.VerbAssignments: s.doAssignments,
|
||||
mma.VerbProposals: s.doProposals,
|
||||
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.VerbAgain: s.doAgain,
|
||||
}
|
||||
var out []stdio.Tool
|
||||
for _, v := range mma.Verbs() {
|
||||
@@ -496,7 +496,7 @@ func (s *seatTools) tools() []stdio.Tool {
|
||||
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.
|
||||
func (s *seatTools) seats(node string) (any, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
stdio.StateRevision
|
||||
Key string
|
||||
}
|
||||
|
||||
// nodeHistory is the revisions of a node's placement, in the order the bus wrote them: its legacy key's
|
||||
// `placement.<node>` before it was converted, then its node key's `<node>.node`, 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's placement history).
|
||||
// nodeHistory is the revisions of a node's assignments, in the order the bus wrote them: its node key's `<node>.node`,
|
||||
// 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's assignments history).
|
||||
func (s *seatTools) nodeHistory(node string) ([]keyedRevision, error) {
|
||||
legacy, err := mmac.LegacyKey(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeKey, err := mmac.NodeKey(node)
|
||||
if err != nil {
|
||||
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
|
||||
for _, key := range []string{legacy, nodeKey} {
|
||||
revisions, err := s.st.history(mma.StatePlacement, key)
|
||||
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})
|
||||
}
|
||||
for _, r := range revisions {
|
||||
out = append(out, keyedRevision{StateRevision: r, Key: nodeKey})
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Revision < out[j].Revision })
|
||||
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).
|
||||
type sketch struct {
|
||||
modules []mma.PlacementByNodeNodeValueModulesItem
|
||||
modules []mma.AssignmentsByNodeNodeValueModulesItem
|
||||
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 {
|
||||
_, _, kind, err := mmac.ParseKey(key)
|
||||
if err != nil || kind != mmac.NodeKind {
|
||||
return sketch{}
|
||||
}
|
||||
n, err := mmac.DecodeNode(value)
|
||||
if err != nil {
|
||||
return sketch{}
|
||||
}
|
||||
wide := func(pins map[string]mma.Provider, holds []mma.Holding) string {
|
||||
raw, _ := json.Marshal([]any{pins, holds})
|
||||
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
|
||||
raw, _ := json.Marshal([]any{n.Pins, n.Holds})
|
||||
return sketch{modules: n.Modules, nodeWide: string(raw)}
|
||||
}
|
||||
|
||||
// changeFrom is what changed from before to s on node, as plc.ChangeOf says it.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -19,16 +20,16 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"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"
|
||||
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"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"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 {
|
||||
@@ -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,
|
||||
// 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.
|
||||
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"}}}
|
||||
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{}}
|
||||
}
|
||||
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) }
|
||||
|
||||
raw, _ := mmac.EncodePlacement(anchor)
|
||||
st[mma.StatePlacement].values["placement.anchor"] = raw
|
||||
setAssignments(st[mma.StateAssignments], anchor)
|
||||
|
||||
// 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")
|
||||
@@ -106,19 +106,19 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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{}
|
||||
pa, err := s.doPlacement(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") {
|
||||
t.Errorf("placement of web = %+v, %v", pa, err)
|
||||
pa, err := s.doAssignments(map[string]any{"module": "web", "history": "true"})
|
||||
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("assignments of web = %+v, %v", pa, err)
|
||||
} 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")
|
||||
}
|
||||
ps, err := s.doProposals(map[string]any{"id": move.ID})
|
||||
@@ -132,7 +132,7 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
t.Errorf("a hold: %v", err)
|
||||
}
|
||||
@@ -168,11 +168,10 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
if err := addNode("novox.mesh-enrolment", good); err != nil {
|
||||
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;
|
||||
// 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{}})
|
||||
st[mma.StatePlacement].values["placement.joiner"] = joiner
|
||||
setAssignments(st[mma.StateAssignments], mma.Assignments{V: keys.Version, Node: "joiner", Decided: mma.NodeDecided{Address: "10.9.0.7"}, Modules: []mma.PlacedModule{}})
|
||||
removeNode := func(node, warrant string) (any, error) {
|
||||
return s.doPropose(map[string]any{"why": "enrolment ended", "warrant": warrant,
|
||||
"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) {
|
||||
w := newWorld()
|
||||
fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("2", 64)
|
||||
pl, _ := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: "ace", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
pkey, _ := mmac.LegacyKey("ace")
|
||||
w.putEntry(plc.PlacementBucket, pkey, pl, 1, time.Now())
|
||||
putAssignments(w, mma.Assignments{V: keys.Version, Node: "ace", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
for node, digest := range map[string]string{"ace": "sha256:" + strings.Repeat("a", 64), "g14": ""} {
|
||||
hn, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Composed: digest})
|
||||
hk, _ := nodeengineclient.HaveNodeKey(node)
|
||||
@@ -220,25 +217,18 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
if _, ok := findings[assign.UnplacedKey("g14")]; !ok || len(findings) != 1 {
|
||||
t.Fatalf("findings = %v", findings)
|
||||
}
|
||||
pl2, _ := mmac.EncodePlacement(mma.Placement{V: keys.Version, Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
pkey2, _ := mmac.LegacyKey("g14")
|
||||
w.putEntry(plc.PlacementBucket, pkey2, pl2, 1, time.Now())
|
||||
putAssignments(w, mma.Assignments{V: keys.Version, Node: "g14", Modules: []mma.PlacedModule{{Module: "web"}}})
|
||||
p.unplacedFindings()
|
||||
if len(findings) != 0 {
|
||||
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
|
||||
kv := &countingKV{memKV: memKV{values: map[string][]byte{}}, writes: &writes}
|
||||
p := &passer{w: newWorld(), logf: t.Logf, kvs: kvSet{plc.SeatHoldersBucket: kv}}
|
||||
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})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k, _ := mmac.LegacyKey(node)
|
||||
p.w.putEntry(plc.PlacementBucket, k, pl, 1, time.Now())
|
||||
putAssignments(p.w, mma.Assignments{V: keys.Version, Node: node, Modules: []mma.PlacedModule{{Module: "web"}, {Module: "nats"}}, Holds: holds})
|
||||
}
|
||||
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"})
|
||||
@@ -249,7 +239,7 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
}
|
||||
p.seatHolders(ctx)
|
||||
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
|
||||
p.seatHolders(ctx)
|
||||
@@ -262,68 +252,122 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
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()
|
||||
kv := &revKV{values: map[string][]byte{}, revs: map[string]uint64{}}
|
||||
var logs []string
|
||||
p := &passer{w: newWorld(), kvs: kvSet{plc.PlacementBucket: kv}, now: time.Now,
|
||||
logf: func(f string, a ...any) { logs = append(logs, fmt.Sprintf(f, a...)) }}
|
||||
put := func(key string, value []byte, rev uint64) {
|
||||
p.w.putEntry(plc.PlacementBucket, key, value, rev, time.Now())
|
||||
p := &passer{w: newWorld(), kvs: kvSet{plc.AssignmentsBucket: kv}, now: time.Now, logf: t.Logf}
|
||||
first := mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}
|
||||
if _, err := p.writeNode(ctx, "anchor", &first); err != nil || strings.Join(kv.log, " ") != "create anchor.web create anchor.node" {
|
||||
t.Fatalf("the first write = %v, %v", kv.log, err)
|
||||
}
|
||||
was := mma.Placement{Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}
|
||||
raw, _ := mmac.EncodePlacement(was)
|
||||
put("placement.anchor", raw, kv.write("placement.anchor", raw))
|
||||
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}}}}
|
||||
raw := kv.values["anchor.node"]
|
||||
// A write: the module keys, the node key (its commit), then the module key no longer placed.
|
||||
after := mma.Assignments{Node: "anchor", Modules: []mma.PlacedModule{{Module: "shell", Ports: map[string]int64{"22": 2222}}}}
|
||||
kv.log = nil
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
// 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
|
||||
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" {
|
||||
t.Fatalf("a refused commit = %v, %v", kv.log, err)
|
||||
}
|
||||
fresh, err := mmac.PlacementsOf(kv.values)
|
||||
if err != nil || !mmac.SamePlacement(fresh["anchor"], after) {
|
||||
fresh, err := mmac.AssignmentsOf(kv.values)
|
||||
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)
|
||||
}
|
||||
kv.refuse = ""
|
||||
// The verb's history reads the form before, then the node key.
|
||||
st := memStore{mma.StatePlacement: &memKV{values: kv.values}}
|
||||
// The verb's history reads the node key.
|
||||
st := memStore{mma.StateAssignments: &memKV{values: kv.values}}
|
||||
hs := newSeatTools(st, nil)
|
||||
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: kv.values["anchor.node"], Revision: 5, Op: "put"}, {Key: "anchor.node", Revision: 9, Op: "del"}},
|
||||
"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"}},
|
||||
}}
|
||||
pa, err := hs.doPlacement(map[string]any{"node": "anchor", "history": "true"})
|
||||
a, _ := pa.(placementAnswer)
|
||||
pa, err := hs.doAssignments(map[string]any{"node": "anchor", "history": "true"})
|
||||
a, _ := pa.(assignmentsAnswer)
|
||||
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"}) ||
|
||||
!a.Changes[2].Deleted || !slices.Equal(a.Changes[2].Change.Removed, []string{"shell"}) {
|
||||
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
|
||||
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.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) {
|
||||
findings := map[string][]byte{}
|
||||
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) {
|
||||
if _, ok := k.values[key]; ok || key == k.refuse {
|
||||
return 0, errors.New("key exists")
|
||||
if _, ok := k.values[key]; ok {
|
||||
return 0, jetstream.ErrKeyExists
|
||||
}
|
||||
if key == k.refuse {
|
||||
return 0, errors.New("refused")
|
||||
}
|
||||
k.log = append(k.log, "create "+key)
|
||||
return k.write(key, value), nil
|
||||
@@ -581,3 +628,71 @@ type historyStore struct {
|
||||
func (s historyStore) history(_, key string) ([]stdio.StateRevision, error) {
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
"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.
|
||||
var marksBucket = placement.Module + "_" + views.MarksState
|
||||
var marksBucket = assignment.Module + "_" + views.MarksState
|
||||
|
||||
// viewedTool is one tool over one state of the assigner's.
|
||||
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
|
||||
// (views.CallersEnv), which the manifest writes.
|
||||
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."},
|
||||
{"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).
|
||||
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_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."},
|
||||
@@ -52,7 +52,7 @@ var readTools = []viewedTool{
|
||||
|
||||
// viewedStates are the states the views family says, each by its bucket: the six its readers read.
|
||||
var viewedStates = map[string]string{
|
||||
mma.StatePlacement: plc.PlacementBucket,
|
||||
mma.StateAssignments: plc.AssignmentsBucket,
|
||||
mma.StateSeatHolders: plc.SeatHoldersBucket,
|
||||
"wants": buckets.Wants,
|
||||
mcc.FindingsState: assign.FindingsBucket,
|
||||
@@ -66,13 +66,13 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
|
||||
marks := state(views.MarksState)
|
||||
var out []stdio.Tool
|
||||
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 {
|
||||
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.
|
||||
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)} }
|
||||
@@ -84,13 +84,13 @@ func startViews(ctx context.Context, js jetstream.JetStream, nc process.Subscrib
|
||||
if findings.Put == nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
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
|
||||
start := func(kv jetstream.KeyValue) {
|
||||
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
|
||||
// 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 {
|
||||
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 {
|
||||
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).
|
||||
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,
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
f := views.UnansweredFinding(placement.Module, s, time.Now())
|
||||
f := views.UnansweredFinding(assignment.Module, s, time.Now())
|
||||
var err error
|
||||
if raised {
|
||||
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
|
||||
// findings state it is said in the log alone.
|
||||
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 {
|
||||
w.Delete = func(key string) error {
|
||||
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"
|
||||
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/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"
|
||||
"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/seats/nodeengine"
|
||||
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"
|
||||
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/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
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-sdk/go/catalogue"
|
||||
|
||||
"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
|
||||
@@ -45,9 +45,9 @@ type world struct {
|
||||
meta map[string]entryMeta
|
||||
// trunkBuilt is, by module, its newest done trunk build in the builder's log (putBuilt).
|
||||
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.
|
||||
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
|
||||
// history keeps (watched with history): a build's source revision names the release that carries it (ADR 0601 §1).
|
||||
sourceGroups map[uint64]string
|
||||
@@ -68,7 +68,7 @@ type parsed struct {
|
||||
|
||||
func newWorld() *world {
|
||||
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{}}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ func (w *world) putEntry(bucket, key string, value []byte, revision uint64, at t
|
||||
return
|
||||
}
|
||||
w.meta[bucket+"|"+key] = entryMeta{revision: revision, at: at}
|
||||
if bucket == plc.PlacementBucket {
|
||||
_, _ = w.placement.Put(key, value, revision, false) // put took it already; this is its revision
|
||||
if bucket == plc.AssignmentsBucket {
|
||||
_, _ = w.assignmentKeys.Put(key, value, revision, false) // put took it already; this is its revision
|
||||
}
|
||||
if bucket == buckets.Source && key == mdc.SourceMainKey {
|
||||
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
|
||||
if value == nil {
|
||||
delete(w.values[bucket], key)
|
||||
if bucket == plc.PlacementBucket {
|
||||
_, _ = w.placement.Put(key, nil, 0, true)
|
||||
if bucket == plc.AssignmentsBucket {
|
||||
_, _ = w.assignmentKeys.Put(key, nil, 0, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.values[bucket][key] = value
|
||||
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.
|
||||
_, _ = w.placement.Put(key, value, 0, false)
|
||||
_, _ = w.assignmentKeys.Put(key, value, 0, false)
|
||||
}
|
||||
if bucket == buckets.Have {
|
||||
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
|
||||
}
|
||||
|
||||
// assigned is each node's modules the seat mesh-module-assigner's placement 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).
|
||||
// assigned is each node's modules the seat mesh-module-assigner's assignments names, in name order (ADR 0549 §5, ADR 0567 §8): the
|
||||
// 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 {
|
||||
placed, _ := w.placed()
|
||||
out := make(map[string][]string, len(placed))
|
||||
@@ -267,23 +267,23 @@ func (w *world) assigned() map[string][]string {
|
||||
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.
|
||||
func (w *world) placed() (map[string][]string, bool) {
|
||||
out := map[string][]string{}
|
||||
for node, p := range w.placements() {
|
||||
for node, p := range w.assignments() {
|
||||
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).
|
||||
func (w *world) placements() map[string]mma.Placement {
|
||||
out, _ := w.placement.AllRevisions()
|
||||
// assignments is every node's assignments, assembled from its keys in either form (mmac.Keys).
|
||||
func (w *world) assignments() map[string]mma.Assignments {
|
||||
out, _ := w.assignmentKeys.AllRevisions()
|
||||
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.
|
||||
func (w *world) placedNodes() map[string]bool {
|
||||
placed, _ := w.placed()
|
||||
@@ -309,8 +309,8 @@ func (w *world) trunk() map[string]meshcatalog.ModulesByModuleValue {
|
||||
return out
|
||||
}
|
||||
|
||||
// records is each node's record in its own have key (`<node>.node`): the nodes a placement proposal is answered by,
|
||||
// with those placement names (ADR 0593 §8).
|
||||
// records is each node's record in its own have key (`<node>.node`): the nodes an assignments proposal is answered by,
|
||||
// with those assignments names (ADR 0593 §8).
|
||||
func (w *world) records() map[string]nodeengine.NodeRecord {
|
||||
out := map[string]nodeengine.NodeRecord{}
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (w *world) unplacedNodes() []string {
|
||||
placed := w.placedNodes()
|
||||
|
||||
@@ -4,9 +4,9 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
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/placement/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-contracts/go/seats/meshmoduleassigner 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/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/seats/meshhandacts 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
|
||||
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
|
||||
replace git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient => ../../contracts/go/clients/meshdeliverycheckerclient
|
||||
|
||||
@@ -2,14 +2,14 @@ package assign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"regexp"
|
||||
"strings"
|
||||
"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/meshdeliveryclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
|
||||
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
|
||||
// data, whose restore ask waits for the operator.
|
||||
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}
|
||||
if !raised {
|
||||
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
|
||||
// (raised false) once the node no longer fails alone.
|
||||
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}
|
||||
if !raised {
|
||||
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
|
||||
// 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 {
|
||||
key := placement.Module + ".neither." + node + "." + module
|
||||
key := assignment.Module + ".neither." + node + "." + module
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
|
||||
if !raised {
|
||||
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),
|
||||
// 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 {
|
||||
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}
|
||||
if !raised {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
|
||||
|
||||
@@ -2,16 +2,16 @@ package assign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"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/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-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"},
|
||||
{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"},
|
||||
{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"},
|
||||
} {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said)
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package assign
|
||||
|
||||
import (
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"strings"
|
||||
|
||||
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
|
||||
// §1): raised naming each, cleared when none is left.
|
||||
func SeatWaitsCondition(unbound []string) oc.Condition {
|
||||
key := placement.Module + ".seat-states"
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: placement.Module}
|
||||
key := assignment.Module + ".seat-states"
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: assignment.Module}
|
||||
if len(unbound) == 0 {
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,13 +21,13 @@ import (
|
||||
// assigner no longer says it straight (never both).
|
||||
|
||||
// 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
|
||||
// `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.
|
||||
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 == "" {
|
||||
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,
|
||||
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)
|
||||
return key, value, true, err
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package assign
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"sort"
|
||||
"strings"
|
||||
"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).
|
||||
|
||||
// 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.
|
||||
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.
|
||||
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 {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
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 + ".",
|
||||
Resolved: node + " is placed", Source: "assigner." + KindUnplaced}, at)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"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"
|
||||
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-builder/builds"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
@@ -75,7 +75,7 @@ func StepPut(w PutWorld) PutNext {
|
||||
}
|
||||
fp := short(p.FP)
|
||||
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 {
|
||||
case builds.BuildDone:
|
||||
|
||||
@@ -10,27 +10,27 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"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"
|
||||
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"
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
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
|
||||
// `placement`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
|
||||
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: assignments is its state
|
||||
// `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.
|
||||
|
||||
// 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
|
||||
// digest.
|
||||
type ProposeIn struct {
|
||||
ID, By, Why string
|
||||
At time.Time
|
||||
Changes []mma.ProposalChange
|
||||
Current map[string]mma.Placement
|
||||
Current map[string]mma.Assignments
|
||||
Revisions map[string]uint64
|
||||
Held func(module 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),
|
||||
// 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
|
||||
// CheckPlacements refuses of the result laid over every placement (two modules on one host port, a provider not placed,
|
||||
// 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
|
||||
// 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
|
||||
// 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(),
|
||||
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
|
||||
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)
|
||||
findings = append(findings, lay...)
|
||||
findings = append(findings, ControllerRefusals(in.Changes, laid)...)
|
||||
all := map[string]mma.Placement{}
|
||||
all := map[string]mma.Assignments{}
|
||||
for node, pl := range in.Current {
|
||||
all[node] = pl
|
||||
}
|
||||
out := map[string]*mma.Placement{}
|
||||
out := map[string]*mma.Assignments{}
|
||||
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
|
||||
}
|
||||
pl := pl
|
||||
@@ -79,7 +79,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
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
|
||||
// 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) {
|
||||
if _, ok := in.Current[node]; !ok {
|
||||
continue
|
||||
@@ -89,7 +89,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
p.Base[node] = int64(in.Revisions[node])
|
||||
}
|
||||
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
|
||||
if held == nil {
|
||||
@@ -110,7 +110,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
was := recorded
|
||||
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)
|
||||
}
|
||||
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).
|
||||
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
|
||||
// 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)"
|
||||
|
||||
// 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.
|
||||
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placement) []string {
|
||||
// whole assignments, or in what the changes lay. None when nothing does.
|
||||
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Assignments) []string {
|
||||
var out []string
|
||||
nodes := map[string]bool{}
|
||||
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,
|
||||
// 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).
|
||||
type Write struct {
|
||||
Node, Key string
|
||||
Base uint64
|
||||
Value *mma.Placement
|
||||
Value *mma.Assignments
|
||||
}
|
||||
|
||||
// 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
|
||||
for _, node := range plc.Nodes(p) {
|
||||
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
|
||||
// module, or node and provision for a pin).
|
||||
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,
|
||||
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.
|
||||
func PlacementAct(p mma.Proposal) occ.Act {
|
||||
return occ.Act{"placement": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
|
||||
// AssignmentsAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
|
||||
func AssignmentsAct(p mma.Proposal) occ.Act {
|
||||
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.
|
||||
func PlacementAsking(p mma.Proposal, at time.Time) int {
|
||||
func AssignmentsAsking(p mma.Proposal, at time.Time) int {
|
||||
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.
|
||||
func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
|
||||
binds, err := occ.ActDigest(PlacementAct(p))
|
||||
func AssignmentsAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
|
||||
binds, err := occ.ActDigest(AssignmentsAct(p))
|
||||
if err != nil {
|
||||
return asks.Ask{}, err
|
||||
}
|
||||
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 {
|
||||
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{
|
||||
ID: meshdeliveryclient.AskingID(key, asking),
|
||||
Headline: headline,
|
||||
Explanation: brief,
|
||||
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},
|
||||
{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
|
||||
// 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.Changes, rest.Base = nil, map[string]int64{}
|
||||
for _, c := range p.Changes {
|
||||
|
||||
@@ -8,23 +8,23 @@ import (
|
||||
"testing"
|
||||
"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"
|
||||
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
|
||||
// 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.
|
||||
func TestThePlacementSeatDecides(t *testing.T) {
|
||||
anchor := mma.Placement{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
|
||||
func TestTheAssignmentsSeatDecides(t *testing.T) {
|
||||
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"}}}
|
||||
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 {
|
||||
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" },
|
||||
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 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 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"},
|
||||
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
|
||||
// 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 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)"},
|
||||
"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{
|
||||
{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"}}},
|
||||
@@ -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.
|
||||
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 {
|
||||
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 {
|
||||
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))
|
||||
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 {
|
||||
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], AssignmentsAct(p)) != nil {
|
||||
t.Fatalf("the ask = %+v, %v", a, err)
|
||||
}
|
||||
q := p
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"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"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
"maps"
|
||||
"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
|
||||
// build names none, and holds nothing.
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -587,11 +587,11 @@ func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string]
|
||||
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 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
|
||||
// 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
|
||||
// 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) {
|
||||
|
||||
@@ -3,8 +3,8 @@ package assign
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"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"
|
||||
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-builder/builds"
|
||||
"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,
|
||||
// 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.
|
||||
|
||||
+23
-23
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"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
|
||||
// which seat.
|
||||
func TestPlacement(t *testing.T) {
|
||||
g14 := seat.Placement{Node: "g14", Revision: "abc",
|
||||
func TestAssignments(t *testing.T) {
|
||||
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"},
|
||||
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"}},
|
||||
@@ -29,13 +29,13 @@ func TestPlacement(t *testing.T) {
|
||||
Pins: map[string]seat.Provider{"shell": {Node: "g14", 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"}}}
|
||||
current := map[string]seat.Placement{"g14": g14, "novox": novox}
|
||||
current := map[string]seat.Assignments{"g14": g14, "novox": novox}
|
||||
const id = "p-20261028t183000-1a2b3c"
|
||||
// the enroller is whichever module holds the seat mesh-node-enroller: no module is named for it
|
||||
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}}}})
|
||||
if !slices.Equal(enrollers, []string{enroller}) {
|
||||
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"}) {
|
||||
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)
|
||||
}
|
||||
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" }
|
||||
enrolled := func(n string) bool { return n != "stranger" }
|
||||
if r := CheckPlacements(current, known, enrolled); len(r) != 0 {
|
||||
t.Fatalf("a sound placement is refused: %v", r)
|
||||
if r := CheckAssignments(current, known, enrolled); len(r) != 0 {
|
||||
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.
|
||||
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.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 {
|
||||
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.Node = "stranger"
|
||||
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",
|
||||
"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",
|
||||
@@ -113,7 +113,7 @@ func TestPlacement(t *testing.T) {
|
||||
t.Fatalf("laid %+v, %v", laid, findings)
|
||||
}
|
||||
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" {
|
||||
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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
if ExportDigest(current) != ExportDigest(map[string]seat.Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
|
||||
t.Error("the export digest depends on order or not on the placements")
|
||||
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 assignments")
|
||||
}
|
||||
// 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.
|
||||
@@ -267,7 +267,7 @@ func TestPlacement(t *testing.T) {
|
||||
AddNodeRefusal(remove, enroller, enrollers) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
|
||||
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 {
|
||||
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) {
|
||||
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"}}},
|
||||
"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"}}},
|
||||
+13
-13
@@ -1,17 +1,17 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"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
|
||||
// `<proposal>.<node>` in its state `candidates`: the node's placement with the proposal laid over it for a node whose 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 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
|
||||
// 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) {
|
||||
proposal, node, ok := strings.Cut(key, ".")
|
||||
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
|
||||
}
|
||||
@@ -40,8 +40,8 @@ func checkCandidate(c seat.CandidatesByProposalNodeValue) error {
|
||||
if err := client.CheckProposalID(c.Proposal); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Placement != nil {
|
||||
return client.CheckPlacement(*c.Placement)
|
||||
if c.Assignments != nil {
|
||||
return client.CheckAssignments(*c.Assignments)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -52,15 +52,15 @@ func EncodeCandidate(c seat.CandidatesByProposalNodeValue) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.V = keys.Version
|
||||
if c.Placement != nil {
|
||||
p := *c.Placement
|
||||
if c.Assignments != nil {
|
||||
p := *c.Assignments
|
||||
p.V = keys.Version
|
||||
c.Placement = &p
|
||||
c.Assignments = &p
|
||||
}
|
||||
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.
|
||||
func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
|
||||
if err := keys.CheckVersion(data); err != nil {
|
||||
@@ -68,7 +68,7 @@ func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
|
||||
}
|
||||
c, err := seat.DecodeCandidatesByProposalNodeValue(data)
|
||||
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 {
|
||||
return seat.CandidatesByProposalNodeValue{}, err
|
||||
+21
-21
@@ -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
|
||||
// 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.
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
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/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"},
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// 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).
|
||||
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
|
||||
nodes := sortedKeys(placements)
|
||||
nodes := sortedKeys(assignments)
|
||||
placed := func(p seat.Provider) bool {
|
||||
other, ok := placements[p.Node]
|
||||
other, ok := assignments[p.Node]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
@@ -52,9 +52,9 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
|
||||
addresses := map[string]string{}
|
||||
var hubs []string
|
||||
for _, node := range nodes {
|
||||
p := placements[node]
|
||||
p := assignments[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 other, twice := addresses[a]; twice {
|
||||
@@ -122,8 +122,8 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
|
||||
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
|
||||
// none) and after (0: the key was deleted), and its modules added, removed or changed against the placement before.
|
||||
// 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 assignments before.
|
||||
type Change struct {
|
||||
Node string `json:"node"`
|
||||
Before uint64 `json:"before"`
|
||||
@@ -141,10 +141,10 @@ func (c Change) Body() map[string]any {
|
||||
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
|
||||
// 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}
|
||||
if c.Node == "" {
|
||||
c.Node = before.Node
|
||||
@@ -176,7 +176,7 @@ func ChangeOf(before, after seat.Placement) Change {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
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{}
|
||||
for _, n := range sortedKeys(placements) {
|
||||
for _, h := range placements[n].Holds {
|
||||
for _, n := range sortedKeys(assignments) {
|
||||
for _, h := range assignments[n].Holds {
|
||||
node := ""
|
||||
if h.Scope == "node" {
|
||||
node = n
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ type tunnelPeerForm struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type placementForm struct {
|
||||
type assignmentsForm struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
@@ -107,7 +107,7 @@ type changeForm struct {
|
||||
Layer map[string]any `json:"layer,omitempty"`
|
||||
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
Whole *placementForm `json:"whole,omitempty"`
|
||||
Whole *assignmentsForm `json:"whole,omitempty"`
|
||||
Decided *decidedForm `json:"decided,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}
|
||||
}
|
||||
|
||||
func placementOf(p seat.Placement) placementForm {
|
||||
out := placementForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
|
||||
func assignmentsOf(p seat.Assignments) assignmentsForm {
|
||||
out := assignmentsForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
|
||||
Pins: providersOf(p.Pins)}
|
||||
if p.Modules != nil {
|
||||
out.Modules = []placedForm{}
|
||||
@@ -170,7 +170,7 @@ func changeOf(c seat.ProposalChange) changeForm {
|
||||
out.Value, _ = json.Marshal(c.Value)
|
||||
}
|
||||
if c.Whole != nil {
|
||||
w := placementOf(*c.Whole)
|
||||
w := assignmentsOf(*c.Whole)
|
||||
out.Whole = &w
|
||||
}
|
||||
if c.Decided != nil {
|
||||
@@ -183,14 +183,14 @@ func changeOf(c seat.ProposalChange) changeForm {
|
||||
return out
|
||||
}
|
||||
|
||||
// ExportDigest is the digest of an export's answer, every node's placement, as the seed proposes it: the same
|
||||
// placements give the same digest, whatever order they came in.
|
||||
func ExportDigest(placements map[string]seat.Placement) string {
|
||||
// ExportDigest is the digest of an export's answer, every node's assignments, as the seed proposes it: the same
|
||||
// assignments give the same digest, whatever order they came in.
|
||||
func ExportDigest(assignments map[string]seat.Assignments) string {
|
||||
h := sha256.New()
|
||||
for _, node := range sortedKeys(placements) {
|
||||
p := placements[node]
|
||||
p.V = keys.Version // the export was made with the placement encoded at the keys' version
|
||||
raw, _ := json.Marshal(placementOf(p))
|
||||
for _, node := range sortedKeys(assignments) {
|
||||
p := assignments[node]
|
||||
p.V = keys.Version // the export was made with the assignments encoded at the keys' version
|
||||
raw, _ := json.Marshal(assignmentsOf(p))
|
||||
fmt.Fprintf(h, "%s\n%s\n", node, raw)
|
||||
}
|
||||
return "sha256:" + hex.EncodeToString(h.Sum(nil))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"os"
|
||||
+43
-43
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
"strings"
|
||||
"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"
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
@@ -67,50 +67,50 @@ func CheckChange(c seat.ProposalChange) error {
|
||||
return needs("field or value", c.Field != "" && c.Value != nil)
|
||||
case client.OpSeed:
|
||||
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:
|
||||
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.
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
case client.OpRemoveNode:
|
||||
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 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;
|
||||
// current is every node's placement now (absent: the node has no key). It returns, by node, the laid placement, and
|
||||
// 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 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
|
||||
// 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.
|
||||
func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange) (map[string]seat.Placement, []string) {
|
||||
laid := map[string]seat.Placement{}
|
||||
func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChange) (map[string]seat.Assignments, []string) {
|
||||
laid := map[string]seat.Assignments{}
|
||||
var findings []string
|
||||
of := func(node string) seat.Placement {
|
||||
of := func(node string) seat.Assignments {
|
||||
if p, ok := laid[node]; ok {
|
||||
return p
|
||||
}
|
||||
p, ok := current[node]
|
||||
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 {
|
||||
if err := CheckChange(c); err != nil {
|
||||
@@ -129,14 +129,14 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
}
|
||||
switch c.Op {
|
||||
case client.OpSeed:
|
||||
p = clonePlacement(*c.Whole)
|
||||
p = cloneAssignments(*c.Whole)
|
||||
case client.OpAddNode:
|
||||
// A joining node has no key yet (ADR 0587 §7): one that has is no joining node.
|
||||
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))
|
||||
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:
|
||||
// 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).
|
||||
@@ -227,7 +227,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
laid[c.Node] = p
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -275,9 +275,9 @@ var decidedFields = func() map[string]bool {
|
||||
return out
|
||||
}()
|
||||
|
||||
func clonePlacement(p seat.Placement) seat.Placement {
|
||||
func cloneAssignments(p seat.Assignments) seat.Assignments {
|
||||
raw, _ := json.Marshal(p)
|
||||
var out seat.Placement
|
||||
var out seat.Assignments
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if out.Modules == nil {
|
||||
out.Modules = []seat.PlacedModule{}
|
||||
@@ -301,33 +301,33 @@ func CheckProposal(p seat.Proposal) error {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
case client.ProposalAnswering, client.ProposalFailed:
|
||||
case client.ProposalAsked:
|
||||
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:
|
||||
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 == "" {
|
||||
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)
|
||||
}
|
||||
@@ -344,25 +344,25 @@ func checkAddNode(p seat.Proposal) error {
|
||||
}
|
||||
if adds == 0 {
|
||||
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
|
||||
}
|
||||
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
|
||||
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 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
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -383,10 +383,10 @@ func AddNodeRefusal(p seat.Proposal, caller string, enrollers []string) string {
|
||||
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.
|
||||
func EnrollersOf(placements map[string]seat.Placement) []string {
|
||||
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(placements)), enroller.Seat, "")
|
||||
func EnrollersOf(assignments map[string]seat.Assignments) []string {
|
||||
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(assignments)), enroller.Seat, "")
|
||||
var out []string
|
||||
for _, h := range holders {
|
||||
if !slices.Contains(out, h.Module) {
|
||||
@@ -422,7 +422,7 @@ func DecodeProposal(data []byte) (seat.Proposal, error) {
|
||||
}
|
||||
p, err := seat.DecodeProposalsByProposalValue(data)
|
||||
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 {
|
||||
return seat.Proposal{}, err
|
||||
+5
-5
@@ -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 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).
|
||||
const (
|
||||
PlacementBucket = Module + "_" + seat.StatePlacement
|
||||
AssignmentsBucket = Module + "_" + seat.StateAssignments
|
||||
ProposalsBucket = Module + "_" + seat.StateProposals
|
||||
CandidatesBucket = Module + "_" + seat.StateCandidates
|
||||
PulsesBucket = Module + "_" + seat.StatePulses
|
||||
@@ -16,8 +16,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// PlacementHistory is the revisions the placement bucket keeps per key: the bus's most (§6).
|
||||
PlacementHistory = 64
|
||||
// AssignmentsHistory is the revisions the assignments bucket keeps per key: the bus's most (§6).
|
||||
AssignmentsHistory = 64
|
||||
// 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).
|
||||
EventDays = 7
|
||||
@@ -7,13 +7,13 @@ package buckets
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
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"
|
||||
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-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.
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// 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.
|
||||
@@ -17,24 +17,24 @@ import (
|
||||
"time"
|
||||
|
||||
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
|
||||
// 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
|
||||
// can shorten it.
|
||||
var AnswerBound = nec.AnswerBound
|
||||
|
||||
// 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
|
||||
// stands, laid the proposal's placements by node (nil removes the node's key) and nodes every node with a record. A
|
||||
// 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 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
|
||||
// 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.
|
||||
func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement, nodes []string) (intended map[string]map[string]bool, must []string) {
|
||||
// assignments it says itself in its answer, marked in the brief as not computed.
|
||||
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{}
|
||||
all := map[string]bool{}
|
||||
for _, n := range nodes {
|
||||
@@ -50,7 +50,7 @@ func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement,
|
||||
before, placed := current[node]
|
||||
after, changesKey := laid[node]
|
||||
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
|
||||
if after != nil {
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"testing"
|
||||
"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
|
||||
// failing, a later answer judging again, the seed failing on any difference.
|
||||
func TestJudge(t *testing.T) {
|
||||
current := map[string]mma.Placement{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
|
||||
laid := map[string]*mma.Placement{
|
||||
current := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
|
||||
laid := map[string]*mma.Assignments{
|
||||
"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail", Ports: map[string]int64{"25": 2525}}, {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"})
|
||||
if !got["anchor"]["mail"] || !got["anchor"]["web"] || len(got["laptop"]) != 0 || len(got["server"]) != 0 ||
|
||||
!slices.Equal(must, []string{"anchor", "laptop", "server"}) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -63,7 +63,7 @@ func TestManifest(t *testing.T) {
|
||||
states[s.Name] = s.History
|
||||
}
|
||||
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) {
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
// 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 ||
|
||||
!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"}) {
|
||||
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).
|
||||
// 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,
|
||||
// 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",
|
||||
"assigner_read_placement", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences",
|
||||
if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assignments", "seat-holders", "wants",
|
||||
"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"}) {
|
||||
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)
|
||||
}
|
||||
// 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
|
||||
for tool, list := range callers {
|
||||
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
|
||||
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) {
|
||||
t.Errorf("emits = %v, want put and its six views", m.Emits)
|
||||
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 the copy, put and its six views", m.Emits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name": "mesh-module-assigner",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"placement",
|
||||
"assignments",
|
||||
"proposals",
|
||||
"propose",
|
||||
"withdraw",
|
||||
@@ -56,11 +56,12 @@
|
||||
"seat:node-engine.manifest-reads"
|
||||
],
|
||||
"emits": [
|
||||
"assignments-copied",
|
||||
"put",
|
||||
"views.asks",
|
||||
"views.differences",
|
||||
"views.findings",
|
||||
"views.placement",
|
||||
"views.assignments",
|
||||
"views.seat-holders",
|
||||
"views.wants"
|
||||
],
|
||||
@@ -68,10 +69,10 @@
|
||||
"assigner_put",
|
||||
"assigner_release",
|
||||
"assigner_puts",
|
||||
"placement",
|
||||
"assignments",
|
||||
"seat-holders",
|
||||
"wants",
|
||||
"assigner_read_placement",
|
||||
"assigner_read_assignments",
|
||||
"assigner_read_seat_holders",
|
||||
"assigner_read_wants",
|
||||
"assigner_read_findings",
|
||||
@@ -82,6 +83,7 @@
|
||||
"assigner_engine_reads"
|
||||
],
|
||||
"state": [
|
||||
{ "name": "assignments", "history": 64 },
|
||||
{ "name": "placement", "history": 64 },
|
||||
{ "name": "proposals", "history": 5 },
|
||||
{ "name": "candidates", "history": 5 },
|
||||
@@ -147,7 +149,7 @@
|
||||
"mesh-delivery-assigner"
|
||||
],
|
||||
"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_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
|
||||
// says the name it had before.
|
||||
func TestNoTextSaysTheOldSeatName(t *testing.T) {
|
||||
old := "mesh-" + "placement"
|
||||
old := "mesh-" + "assignments"
|
||||
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" {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user