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
|
||||
|
||||
@@ -5,8 +5,8 @@ go 1.26.0
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
|
||||
@@ -4,9 +4,9 @@ package buildrun
|
||||
// builder checks them against it) and from the build seats' states: a bucket is `<holder module>_<state>`, and no contract
|
||||
// names a holder (novox/hq ADR 0625 §4, ADR 0634 §1).
|
||||
const (
|
||||
// PlacementBucket is the assigner's state placement, which the toolchain gate reads for the placing of a build module.
|
||||
AssignerModule = "mesh-delivery-assigner"
|
||||
PlacementBucket = AssignerModule + "_placement"
|
||||
// AssignmentsBucket is the assigner's state assignments, which the toolchain gate reads for the placing of a build module.
|
||||
AssignerModule = "mesh-delivery-assigner"
|
||||
AssignmentsBucket = AssignerModule + "_assignments"
|
||||
)
|
||||
|
||||
// StepsBucket is the bucket of a build module's state steps.
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
// ace.typescript at v26.12.0, and every Node fingerprint said "builders disagree").
|
||||
//
|
||||
// ToolchainGate stands between the toolchains buckets and what takes their entries in (the indexer's PutToolchain, a
|
||||
// ToolchainSet): an entry is passed on only while the placement (the seat mesh-placement's state `placement`, held by
|
||||
// the assigner, PlacementBucket, read by meshmoduleassignerclient.Placed; ADR 0567, ADR 0549 §3) places its build module on
|
||||
// its node, and only once it was written since that placement; it is passed on as deleted otherwise, said once, and
|
||||
// passed on when its node writes it again. A report written before the placement is one an earlier run of the module
|
||||
// ToolchainSet): an entry is passed on only while the assignments (the seat mesh-module-assigner's state `assignments`, held by
|
||||
// the assigner, AssignmentsBucket, read by meshmoduleassignerclient.Placed; ADR 0567, ADR 0549 §3) places its build module on
|
||||
// its node, and only once it was written since that assignments; it is passed on as deleted otherwise, said once, and
|
||||
// passed on when its node writes it again. A report written before the assignments is one an earlier run of the module
|
||||
// left (issue 1940, 2026-10-25 20:56: mesh-build-node put on ace again brought ace.typescript v26.12.0 of 2026-10-23
|
||||
// back into the agreement before the module ran there). Until the placement is settled, and while no node's placement
|
||||
// back into the agreement before the module ran there). Until the assignments is settled, and while no node's assignments
|
||||
// is held (before the assigner's adopt writes the first one, or where the state cannot be read), every entry is passed
|
||||
// on. Builders that run and report different versions still disagree: that refusal is the guard of novox/hq ADR 0465,
|
||||
// and it stays.
|
||||
@@ -41,11 +41,11 @@ type ToolchainGate struct {
|
||||
forward func(module, key string, value []byte, deleted bool) bool // answers whether it changed what it holds
|
||||
logf func(format string, a ...any)
|
||||
placed *meshmoduleassignerclient.Placed
|
||||
known bool // the placement is settled
|
||||
known bool // the assignments is settled
|
||||
raw map[string]map[string][]byte // module -> toolchains key -> value, as the bucket holds it
|
||||
left map[string]string // <module>/<key> -> why it was said left out, so each reason is said once
|
||||
// OnPlaced is told, outside the gate's lock, once the placement's history is read and after each revision of it
|
||||
// FollowPlacement hears from then on (the counted nodes, Nodes, may have changed). Nil tells nobody.
|
||||
// OnPlaced is told, outside the gate's lock, once the assignments's history is read and after each revision of it
|
||||
// FollowAssignments hears from then on (the counted nodes, Nodes, may have changed). Nil tells nobody.
|
||||
OnPlaced func()
|
||||
}
|
||||
|
||||
@@ -75,28 +75,28 @@ func (g *ToolchainGate) Toolchain(module, key string, value []byte, deleted bool
|
||||
return g.pass(module, key)
|
||||
}
|
||||
|
||||
// Placement takes one revision of the seat mesh-placement's placement in (at is when the bus wrote it) and answers whether what it
|
||||
// Assignments takes one revision of the seat mesh-module-assigner's assignments in (at is when the bus wrote it) and answers whether what it
|
||||
// passed on changed. A value that does not read is said, and places nothing on its node.
|
||||
func (g *ToolchainGate) Placement(key string, value []byte, deleted bool, at time.Time) bool {
|
||||
func (g *ToolchainGate) Assignments(key string, value []byte, deleted bool, at time.Time) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
was := g.placed.Seeded()
|
||||
node, changed, err := g.placed.Put(key, value, deleted, at)
|
||||
if err != nil {
|
||||
g.logf("%s: %v (issue 1940)", PlacementBucket, err)
|
||||
g.logf("%s: %v (issue 1940)", AssignmentsBucket, err)
|
||||
}
|
||||
if !g.known {
|
||||
return false
|
||||
}
|
||||
if was != g.placed.Seeded() {
|
||||
// The first placement came, or the last went: every report's standing changed.
|
||||
// The first assignments came, or the last went: every report's standing changed.
|
||||
return g.all()
|
||||
}
|
||||
return g.again(node, changed)
|
||||
}
|
||||
|
||||
// Nodes are the nodes the placement places any module on, sorted: the nodes the mesh counts (a node leaves the count
|
||||
// when the placement no longer names it; ADR 0624 §1). None until the placement is read.
|
||||
// Nodes are the nodes the assignments places any module on, sorted: the nodes the mesh counts (a node leaves the count
|
||||
// when the assignments no longer names it; ADR 0624 §1). None until the assignments is read.
|
||||
func (g *ToolchainGate) Nodes() []string {
|
||||
var out []string
|
||||
for node, modules := range g.placed.ByNode() {
|
||||
@@ -108,8 +108,8 @@ func (g *ToolchainGate) Nodes() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// Settled says the placement's history is read: from now on only the reports of placed nodes pass, once a node's
|
||||
// placement is held. It answers whether what it passed on changed.
|
||||
// Settled says the assignments's history is read: from now on only the reports of placed nodes pass, once a node's
|
||||
// assignments is held. It answers whether what it passed on changed.
|
||||
func (g *ToolchainGate) Settled() bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
@@ -144,7 +144,7 @@ func (g *ToolchainGate) again(node string, modules []string) bool {
|
||||
return changed
|
||||
}
|
||||
|
||||
// pass hands one key on as it stands: its value when its node is placed and wrote it since (or the placement is not
|
||||
// pass hands one key on as it stands: its value when its node is placed and wrote it since (or the assignments is not
|
||||
// known), deleted otherwise. The caller holds mu.
|
||||
func (g *ToolchainGate) pass(module, key string) bool {
|
||||
value, held := g.raw[module][key]
|
||||
@@ -183,24 +183,24 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// FollowPlacement reads the seat mesh-placement's placement with its history into gate, says it settled, and follows it; changed
|
||||
// FollowAssignments reads the seat mesh-module-assigner's assignments with its history into gate, says it settled, and follows it; changed
|
||||
// is told whenever what the gate passes on changed. Where the state cannot be read or watched (not there, or this part
|
||||
// may not read it) it says so and the gate passes every report on: not fatal, since an indexer, builder or checker that
|
||||
// stops for it stops every delivery (issue 1945). An error of the watch after that ends on the channel.
|
||||
func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *ToolchainGate, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
|
||||
func FollowAssignments(ctx context.Context, js process.BucketOpener, gate *ToolchainGate, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) {
|
||||
errs := make(chan error, 1)
|
||||
kv, err := js.KeyValue(ctx, PlacementBucket)
|
||||
kv, err := js.KeyValue(ctx, AssignmentsBucket)
|
||||
if err != nil {
|
||||
logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err)
|
||||
logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", AssignmentsBucket, err)
|
||||
return errs, nil
|
||||
}
|
||||
w, err := kv.WatchAll(ctx, jetstream.IncludeHistory())
|
||||
if err != nil {
|
||||
logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err)
|
||||
logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", AssignmentsBucket, err)
|
||||
return errs, nil
|
||||
}
|
||||
put := func(e jetstream.KeyValueEntry) bool {
|
||||
return gate.Placement(e.Key(), e.Value(), e.Operation() != jetstream.KeyValuePut, e.Created())
|
||||
return gate.Assignments(e.Key(), e.Value(), e.Operation() != jetstream.KeyValuePut, e.Created())
|
||||
}
|
||||
for settled := false; !settled; {
|
||||
select {
|
||||
@@ -209,7 +209,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha
|
||||
return errs, ctx.Err()
|
||||
case e, ok := <-w.Updates():
|
||||
if !ok {
|
||||
return errs, errors.New("the watch of " + PlacementBucket + " ended before it settled")
|
||||
return errs, errors.New("the watch of " + AssignmentsBucket + " ended before it settled")
|
||||
}
|
||||
if e == nil {
|
||||
settled = true
|
||||
@@ -233,7 +233,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha
|
||||
return
|
||||
case e, ok := <-w.Updates():
|
||||
if !ok {
|
||||
errs <- errors.New("the watch of " + PlacementBucket + " ended")
|
||||
errs <- errors.New("the watch of " + AssignmentsBucket + " ended")
|
||||
return
|
||||
}
|
||||
if e != nil && put(e) {
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
@@ -27,14 +27,14 @@ func gateReport(t *testing.T, node, version string, at time.Time) (string, []byt
|
||||
return key, value
|
||||
}
|
||||
|
||||
// placing is a value of the state `placement` placing modules on node.
|
||||
// placing is a value of the state `assignments` placing modules on node.
|
||||
func placing(t *testing.T, node string, modules ...string) (string, []byte) {
|
||||
t.Helper()
|
||||
p := meshmoduleassigner.Placement{Node: node}
|
||||
p := meshmoduleassigner.Assignments{Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
values, err := meshmoduleassignerclient.PlacementValues(p)
|
||||
values, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -42,29 +42,14 @@ func placing(t *testing.T, node string, modules ...string) (string, []byte) {
|
||||
return key, values[key]
|
||||
}
|
||||
|
||||
// placingLegacy is placing in the form before, `placement.<node>` (research 072 row 37).
|
||||
func placingLegacy(t *testing.T, node string, modules ...string) (string, []byte) {
|
||||
t.Helper()
|
||||
p := meshmoduleassigner.Placement{Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
key, _ := meshmoduleassignerclient.LegacyKey(node)
|
||||
value, err := meshmoduleassignerclient.EncodePlacement(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return key, value
|
||||
}
|
||||
|
||||
func agreedOf(set *ToolchainSet) (string, string) {
|
||||
tc, none := buildstep.AgreedToolchain(set.Entries("typescript"))
|
||||
return tc.Version, none
|
||||
}
|
||||
|
||||
// Issue 1940, the gate over the catalogue's placement (issue 774, ADR 0549 §3): a build node's report counts only while
|
||||
// the placement places its build module on its node, and only once written since; before the placement is settled, and
|
||||
// while no node's placement is held, every report counts. The live cases: mesh-build-node taken off ace left
|
||||
// Issue 1940, the gate over the catalogue's assignments (issue 774, ADR 0549 §3): a build node's report counts only while
|
||||
// the assignments places its build module on its node, and only once written since; before the assignments is settled, and
|
||||
// while no node's assignments is held, every report counts. The live cases: mesh-build-node taken off ace left
|
||||
// ace.typescript at v26.12.0 against v26.12.1 on the three others; put on ace again, its report of 2026-10-23 06:29 came
|
||||
// back before the module ran there.
|
||||
func TestToolchainGate(t *testing.T) {
|
||||
@@ -78,7 +63,7 @@ func TestToolchainGate(t *testing.T) {
|
||||
for _, node := range three {
|
||||
if seeded {
|
||||
key, value := placing(t, node, NodeBuildModule)
|
||||
gate.Placement(key, value, false, old.Add(-time.Hour))
|
||||
gate.Assignments(key, value, false, old.Add(-time.Hour))
|
||||
}
|
||||
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
@@ -91,10 +76,10 @@ func TestToolchainGate(t *testing.T) {
|
||||
key, value := gateReport(t, "ace", "v26.12.0", old)
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out
|
||||
t.Fatalf("before the placement is settled every report counts; got %+v", a)
|
||||
t.Fatalf("before the assignments is settled every report counts; got %+v", a)
|
||||
}
|
||||
k, v := placing(t, "ace", GoBuildModule) // another build module on ace: no matter
|
||||
gate.Placement(k, v, false, old)
|
||||
gate.Assignments(k, v, false, old)
|
||||
if !gate.Settled() {
|
||||
t.Fatal("settling left ace's report out, so what was passed on changed")
|
||||
}
|
||||
@@ -109,13 +94,13 @@ func TestToolchainGate(t *testing.T) {
|
||||
}
|
||||
// Placed on ace with a report written since: it counts, and the builders really disagree.
|
||||
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
|
||||
gate.Placement(k, v, false, old.Add(-time.Minute))
|
||||
gate.Assignments(k, v, false, old.Add(-time.Minute))
|
||||
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" {
|
||||
t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a)
|
||||
}
|
||||
// Taken off ace again (its key deleted): its report goes again.
|
||||
if !gate.Placement(k, nil, true, placed) {
|
||||
t.Fatal("taking ace's placement away passed nothing on")
|
||||
if !gate.Assignments(k, nil, true, placed) {
|
||||
t.Fatal("taking ace's assignments away passed nothing on")
|
||||
}
|
||||
if v, none := agreedOf(set); v != "v26.12.1" || none != "" {
|
||||
t.Fatalf("got %q, %q", v, none)
|
||||
@@ -132,26 +117,26 @@ func TestToolchainGate(t *testing.T) {
|
||||
at time.Time // when ace wrote its report
|
||||
counts bool
|
||||
}{
|
||||
{"a report left before the placement", old, false},
|
||||
{"a report written since the placement", placed.Add(30 * time.Second), true},
|
||||
{"a report left before the assignments", old, false},
|
||||
{"a report written since the assignments", placed.Add(30 * time.Second), true},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
gate, set, said := start(t, true)
|
||||
k, v := placing(t, "ace") // ace's key, first heard placing nothing
|
||||
gate.Placement(k, v, false, old.Add(-time.Hour))
|
||||
gate.Assignments(k, v, false, old.Add(-time.Hour))
|
||||
gate.Settled()
|
||||
k, v = placing(t, "ace", NodeBuildModule)
|
||||
if gate.Placement(k, v, false, placed) {
|
||||
t.Fatal("a placement with no report passed something on")
|
||||
if gate.Assignments(k, v, false, placed) {
|
||||
t.Fatal("assignments with no report passed something on")
|
||||
}
|
||||
key, value := gateReport(t, "ace", "v26.12.0", c.at)
|
||||
if got := gate.Toolchain(NodeBuildModule, key, value, false); got != c.counts {
|
||||
t.Fatalf("ace's report passed on: %v, want %v", got, c.counts)
|
||||
}
|
||||
// ace's placement written again with another module beside: its standing does not move, nothing is said again.
|
||||
// ace's assignments written again with another module beside: its standing does not move, nothing is said again.
|
||||
k, v = placing(t, "ace", GoBuildModule, NodeBuildModule)
|
||||
if gate.Placement(k, v, false, placed.Add(time.Minute)) {
|
||||
t.Fatal("the same placement of mesh-build-node changed what was passed on")
|
||||
if gate.Assignments(k, v, false, placed.Add(time.Minute)) {
|
||||
t.Fatal("the same assignments of mesh-build-node changed what was passed on")
|
||||
}
|
||||
if c.counts {
|
||||
return
|
||||
@@ -169,47 +154,38 @@ func TestToolchainGate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("no placement held: every report counts", func(t *testing.T) {
|
||||
t.Run("no assignments held: every report counts", func(t *testing.T) {
|
||||
gate, set, _ := start(t, false)
|
||||
key, value := gateReport(t, "ace", "v26.12.0", old)
|
||||
gate.Toolchain(NodeBuildModule, key, value, false)
|
||||
gate.Settled()
|
||||
if n := len(set.Entries("typescript")); n != 4 {
|
||||
t.Fatalf("before the first placement file every report counts; got %d", n)
|
||||
t.Fatalf("before the first assignments file every report counts; got %d", n)
|
||||
}
|
||||
k, v := placing(t, "g14", NodeBuildModule)
|
||||
if !gate.Placement(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 {
|
||||
t.Fatalf("the first placement leaves every node it does not place out; got %d", len(set.Entries("typescript")))
|
||||
if !gate.Assignments(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 {
|
||||
t.Fatalf("the first assignments leaves every node it does not place out; got %d", len(set.Entries("typescript")))
|
||||
}
|
||||
if !gate.Placement(k, nil, true, placed) || len(set.Entries("typescript")) != 4 {
|
||||
t.Fatal("the last placement gone: every report counts again")
|
||||
if !gate.Assignments(k, nil, true, placed) || len(set.Entries("typescript")) != 4 {
|
||||
t.Fatal("the last assignments gone: every report counts again")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) {
|
||||
p := meshmoduleassignerclient.NewPlaced()
|
||||
// ace in the form before, then converted: its module stands since before, the legacy key read no more.
|
||||
k, v := placingLegacy(t, "ace", NodeBuildModule)
|
||||
// ace's node key is the first revision heard of it: its module stands since before.
|
||||
k, v := placing(t, "ace", NodeBuildModule)
|
||||
p.Put(k, v, false, old)
|
||||
k, v = placing(t, "ace", NodeBuildModule)
|
||||
if _, changed, err := p.Put(k, v, false, placed); len(changed) != 0 || err != nil {
|
||||
t.Errorf("the conversion of ace's key changed %v, %v", changed, err)
|
||||
}
|
||||
if _, changed, _ := p.Put("ace."+NodeBuildModule, []byte("{}"), false, placed); len(changed) != 0 {
|
||||
t.Errorf("a module key says which modules stand: %v", changed)
|
||||
}
|
||||
if k, v := placingLegacy(t, "ace"); true {
|
||||
if _, changed, _ := p.Put(k, v, false, placed); len(changed) != 0 {
|
||||
t.Errorf("the legacy key is read after the node key: %v", changed)
|
||||
}
|
||||
}
|
||||
k2, v2 := placing(t, "g14")
|
||||
p.Put(k2, v2, false, old)
|
||||
k2, v2 = placing(t, "g14", NodeBuildModule)
|
||||
if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != NodeBuildModule {
|
||||
t.Errorf("a module placed says changed %v", changed)
|
||||
}
|
||||
if _, _, err := p.Put("placement.novox", []byte("not json"), false, old); err == nil {
|
||||
if _, _, err := p.Put("novox.node", []byte("not json"), false, old); err == nil {
|
||||
t.Error("a value that does not read was not said")
|
||||
}
|
||||
if s, ok := p.Since("ace", NodeBuildModule); !ok || !s.IsZero() {
|
||||
@@ -223,14 +199,14 @@ func TestToolchainGate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a placement the bus refuses is said, and the part goes on", func(t *testing.T) {
|
||||
t.Run("assignments the bus refuses is said, and the part goes on", func(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
logger := log.New(&logs, "", 0)
|
||||
refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement")
|
||||
refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments")
|
||||
gate, _, _ := start(t, false)
|
||||
for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} {
|
||||
if _, err := FollowPlacement(context.Background(), bus, gate, func(context.Context) {}, logger.Printf); err != nil {
|
||||
t.Errorf("a refused placement stopped the start: %v", err)
|
||||
if _, err := FollowAssignments(context.Background(), bus, gate, func(context.Context) {}, logger.Printf); err != nil {
|
||||
t.Errorf("a refused assignments stopped the start: %v", err)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") {
|
||||
|
||||
@@ -303,7 +303,7 @@ func startBedWith(t *testing.T, closed bool) *bed {
|
||||
viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...)
|
||||
}
|
||||
b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...)
|
||||
viewstest.Serve(t, ctx, js, ctl, assignerModule, "placement", assignerModule+".assigner_read_placement", readers...)
|
||||
viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...)
|
||||
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
|
||||
b.instances = mk(instancesBucket, 1)
|
||||
// The tag asks and their bounds (design 51 *Tagged versions*): the bounds declared key-ttl, so an ended bound leaves the
|
||||
|
||||
@@ -72,7 +72,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The indexer's modules, manifests, candidates and packages and the assigner's placement are their owners' states, read by the
|
||||
// The indexer's modules, manifests, candidates and packages and the assigner's assignments are their owners' states, read by the
|
||||
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, DLV-M1). An owner that
|
||||
// is down never holds the start: each is read on a goroutine once its first view is answered, and until then the
|
||||
// unanswered first view is the builder's finding.
|
||||
@@ -105,7 +105,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
placement, err := reader.Follow(assignerModule, "placement", assignerModule+".assigner_read_placement")
|
||||
assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -258,11 +258,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// The toolchains first, so a build that starts from the first modules value finds the nodes.
|
||||
watch := newToolchainWatch(ctx, js, tools, b.NodesChanged, logger)
|
||||
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
|
||||
// placement is settled first, so the toolchains settle through the gate already knowing it.
|
||||
// assignments is settled first, so the toolchains settle through the gate already knowing it.
|
||||
gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
|
||||
watch.put = gate.Toolchain
|
||||
// What each counted node's engine reads (ADR 0618 §2, ADR 0624 §1): asked through the node-engine seat's verb as the
|
||||
// builder's own account, for every node the placement names, and kept in the state engine-reads once it is bound.
|
||||
// builder's own account, for every node the assignments names, and kept in the state engine-reads once it is bound.
|
||||
reads := newEngineReads(calls.CallOn, gate, logger.Printf)
|
||||
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
|
||||
// reads it and drops a retired node by hand.
|
||||
@@ -288,20 +288,20 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
})
|
||||
gate.OnPlaced = reads.placed
|
||||
b.Forms = reads.built
|
||||
// Until the placement's first view is answered the gate passes every report on, as before the placement is settled.
|
||||
placementErrs := make(chan error, 1)
|
||||
// Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
|
||||
assignmentsErrs := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-placement.Follower().Ready():
|
||||
case <-assignments.Follower().Ready():
|
||||
}
|
||||
pe, err := buildrun.FollowPlacement(ctx, reader, gate, b.NodesChanged, logger.Printf)
|
||||
pe, err := buildrun.FollowAssignments(ctx, reader, gate, b.NodesChanged, logger.Printf)
|
||||
if err != nil {
|
||||
placementErrs <- err
|
||||
assignmentsErrs <- err
|
||||
return
|
||||
}
|
||||
placementErrs <- <-pe
|
||||
assignmentsErrs <- <-pe
|
||||
}()
|
||||
if err := watch.start(); err != nil {
|
||||
return err
|
||||
@@ -456,7 +456,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return nil
|
||||
case err := <-errs:
|
||||
return err
|
||||
case err := <-placementErrs:
|
||||
case err := <-assignmentsErrs:
|
||||
return err
|
||||
case err := <-watch.errs:
|
||||
return err
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
|
||||
// indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows.
|
||||
indexerModule = "mesh-delivery-indexer"
|
||||
// assignerModule is the holder of the state placement.
|
||||
// assignerModule is the holder of the state assignments.
|
||||
assignerModule = buildrun.AssignerModule
|
||||
|
||||
// sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names.
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestTheNamesAreTheManifests(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates",
|
||||
indexerModule + ".views.packages", assignerModule + ".views.placement"} {
|
||||
indexerModule + ".views.packages", assignerModule + ".views.assignments"} {
|
||||
if !slices.Contains(m.Consumes, view) {
|
||||
t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestTheNamesAreTheManifests(t *testing.T) {
|
||||
for want, got := range map[string]string{
|
||||
"mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"),
|
||||
"mesh-build-node_toolchains": buildrun.ToolchainsBucket("mesh-build-node"), "mesh-delivery-indexer_packages": packagesBucket,
|
||||
"mesh-delivery-assigner_placement": buildrun.PlacementBucket,
|
||||
"mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket,
|
||||
} {
|
||||
if want != got {
|
||||
t.Errorf("a bucket is %q, want %q", got, want)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// oldForm keeps the old reference form's window (design 51 *Every build keeps the old reference form*, DLV-B9) in the
|
||||
// state old-form: stepped on every change of what the counted nodes state, of the placement, on a hand drop and at each
|
||||
// state old-form: stepped on every change of what the counted nodes state, of the assignments, on a hand drop and at each
|
||||
// build key written, never on a timer. A node holding it seven days is the finding old-form-held; its close is said once
|
||||
// as version.old-form-closed and in the builder's log.
|
||||
type oldForm struct {
|
||||
@@ -84,7 +84,7 @@ func (o *oldForm) dropped() map[string]bool {
|
||||
}
|
||||
|
||||
// step moves the window to what counted states.
|
||||
// named is every node the read named (the placement's nodes before any is dropped), counted the statements of those
|
||||
// named is every node the read named (the assignments's nodes before any is dropped), counted the statements of those
|
||||
// still counted: a read naming no node judges nothing (window.StepWindow), so it never closes the window.
|
||||
func (o *oldForm) step(ctx context.Context, named []string, counted map[string][]string) {
|
||||
o.mu.Lock()
|
||||
@@ -106,7 +106,7 @@ func (o *oldForm) step(ctx context.Context, named []string, counted map[string][
|
||||
n, next.Since[n].Format(time.RFC3339)),
|
||||
headline: "A node holds the old reference form",
|
||||
explanation: "The builder writes an older reference form into every build key while a counted node's engine reads none of the newest (ADR 0624 §1); this node has held it seven days.",
|
||||
resolved: "The node's engine states the newest form, the node leaves the placement, or it is dropped as retired.",
|
||||
resolved: "The node's engine states the newest form, the node leaves the assignments, or it is dropped as retired.",
|
||||
needs: "builder_old_form shows the window; a retired node is dropped there by hand; builder_engine_reads asks the node again."})
|
||||
}
|
||||
o.overdue = overdue
|
||||
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
)
|
||||
|
||||
// EngineReadsState is the builder's state of what each counted node's engine states it reads (novox/hq ADR 0618 §2, ADR
|
||||
// 0624 §1): one key `<node>`, written from the node-engine seat's verb manifest-reads at the builder's start, on the
|
||||
// seat's event manifest-reads-changed for that node, and when a person asks again (builder_engine_reads); a node that
|
||||
// is away keeps its last statement, and its key is removed when the placement no longer names the node. A counted node
|
||||
// is away keeps its last statement, and its key is removed when the assignments no longer names the node. A counted node
|
||||
// with no key reads as stating the baseline: its first forms. No node's record is read in its place.
|
||||
const EngineReadsState = "engine-reads"
|
||||
|
||||
@@ -43,7 +43,7 @@ func newEngineReads(call enginereads.Caller, gate *buildrun.ToolchainGate, logf
|
||||
}
|
||||
|
||||
// stepWindow moves the old form's window to what the counted nodes state now (design 51 *Every build keeps the old
|
||||
// reference form*): on a statement's change, the placement's and a hand drop, and at each build key written.
|
||||
// reference form*): on a statement's change, the assignments's and a hand drop, and at each build key written.
|
||||
func (r *engineReads) stepWindow() {
|
||||
if r.window == nil {
|
||||
return
|
||||
@@ -56,7 +56,7 @@ func (r *engineReads) stepWindow() {
|
||||
r.window.step(ctx, r.named(), counted)
|
||||
}
|
||||
|
||||
// named are the nodes the placement names, none dropped: what the window's step judges a read by.
|
||||
// named are the nodes the assignments names, none dropped: what the window's step judges a read by.
|
||||
func (r *engineReads) named() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
@@ -86,7 +86,7 @@ func (r *engineReads) bind(ctx context.Context, kv jetstream.KeyValue, errs chan
|
||||
}
|
||||
}
|
||||
|
||||
// placed takes the placement in: a node it names newly is asked, one it no longer names is forgotten, key and all. The
|
||||
// placed takes the assignments in: a node it names newly is asked, one it no longer names is forgotten, key and all. The
|
||||
// first call asks every counted node (the builder's start).
|
||||
func (r *engineReads) placed() {
|
||||
nodes := r.gate.Nodes()
|
||||
@@ -96,7 +96,7 @@ func (r *engineReads) placed() {
|
||||
r.mu.Unlock()
|
||||
for _, node := range nodes {
|
||||
if first || !slices.Contains(before, node) {
|
||||
r.keeper.AskNode(node, "counted: the placement names it")
|
||||
r.keeper.AskNode(node, "counted: the assignments names it")
|
||||
}
|
||||
}
|
||||
for _, node := range before {
|
||||
|
||||
@@ -20,15 +20,15 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
"mesh-delivery-builder/internal/versions"
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
|
||||
// 1945 (a part exits at start for a bucket it works without): a bus that refuses every toolchains bucket leaves the
|
||||
// watch unstarted and the part running, said; a refused bucket it cannot work without is an error naming the bucket and
|
||||
// its grant. The gate over the seat's placement, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
||||
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
||||
// issue 774).
|
||||
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
|
||||
@@ -69,28 +69,28 @@ func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
t.Run("ADR 0624: the counted nodes' engine reads", theCountedNodesEngineReads)
|
||||
}
|
||||
|
||||
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the placement names; a node away
|
||||
// theCountedNodesEngineReads: the builder asks the node-engine seat's verb of every node the assignments names; a node away
|
||||
// and never asked has no statement and reads as the baseline's first forms, with no node record read in its place; an
|
||||
// away node keeps its last statement; a node the placement no longer names leaves the count, its key removed; and
|
||||
// away node keeps its last statement; a node the assignments no longer names leaves the count, its key removed; and
|
||||
// builder_engine_reads drops one node's key.
|
||||
func theCountedNodesEngineReads(t *testing.T) {
|
||||
gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf)
|
||||
place := func(nodes ...string) {
|
||||
for _, node := range []string{"n1", "n2"} {
|
||||
pl := meshmoduleassigner.Placement{Node: node}
|
||||
pl := meshmoduleassigner.Assignments{Node: node}
|
||||
if slices.Contains(nodes, node) {
|
||||
pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}}
|
||||
}
|
||||
values, err := meshmoduleassignerclient.PlacementValues(pl)
|
||||
values, err := meshmoduleassignerclient.AssignmentsValues(pl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for key, raw := range values {
|
||||
gate.Placement(key, raw, false, time.Now())
|
||||
gate.Assignments(key, raw, false, time.Now())
|
||||
}
|
||||
if !slices.Contains(nodes, node) {
|
||||
key, _ := meshmoduleassignerclient.ModuleKey(node, "web")
|
||||
gate.Placement(key, nil, true, time.Now())
|
||||
gate.Assignments(key, nil, true, time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func theCountedNodesEngineReads(t *testing.T) {
|
||||
if _, counted = r.forms(); !slices.Equal(counted["n2"], []string{"form:new"}) || r.keeper.Statements()["n2"].Answered {
|
||||
t.Errorf("away again, n2 reads %v (%+v)", counted["n2"], r.keeper.Statements()["n2"])
|
||||
}
|
||||
// n2 retired: it leaves the placement, and its key goes.
|
||||
// n2 retired: it leaves the assignments, and its key goes.
|
||||
place("n1")
|
||||
r.placed()
|
||||
if _, counted = r.forms(); len(counted) != 1 || kept["n2"] != nil {
|
||||
|
||||
@@ -10,8 +10,8 @@ require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
repotrunk "git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
|
||||
@@ -92,7 +92,7 @@ type Builder struct {
|
||||
// Tags answers a check's build's tag from the kept tag asks; nil numbers no check's build.
|
||||
Tags TagOf
|
||||
// Forms answers what each node's engine states it reads (ADR 0618 §2, ADR 0624 §1): present, the nodes whose engine
|
||||
// answered when last asked, and counted, every node the placement names, present or away, a node with no statement
|
||||
// answered when last asked, and counted, every node the assignments names, present or away, a node with no statement
|
||||
// stating nothing (the baseline's first forms). A done key names each artifact in the forms these keep, the oldest in
|
||||
// its single reference, and its needs. Nil: no node states anything, and the first forms are kept.
|
||||
Forms func() (present, counted map[string][]string)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// builder's state engine-reads; this is only the window over them: open while a counted node-engine, present or away,
|
||||
// reads of some artifact kind none of the newest form the builder knows for it, so an older form stays in new build keys
|
||||
// for it; closed, said once with `old-form-closed` and a log line, when none does. It moves on what moves a statement
|
||||
// (the node-engine seat's verb `manifest-reads` and its event `manifest-reads-changed`), the placement, a hand drop and
|
||||
// (the node-engine seat's verb `manifest-reads` and its event `manifest-reads-changed`), the assignments, a hand drop and
|
||||
// each build key written; never on a timer.
|
||||
|
||||
// OldFormState is the builder's state of the window: one key, OldFormKey, an OldFormWindow. builder_old_form reads it.
|
||||
@@ -94,7 +94,7 @@ const FindingAfter = 7 * 24 * time.Hour
|
||||
|
||||
// Step moves the window by mesh-sdk's one window rule (window.StepWindow): named is every counted node the read named,
|
||||
// those dropped by hand included, and holding those of them holding the window (Holding). A read naming no node (no
|
||||
// placement read yet, an empty node_haves) judges nothing, so it never closes the window. It answers the new window,
|
||||
// assignments read yet, an empty node_haves) judges nothing, so it never closes the window. It answers the new window,
|
||||
// whether it closed in this step (so the builder says `old-form-closed` and its log line once), and the nodes holding it
|
||||
// past FindingAfter (the finding, cleared for every other node).
|
||||
func (w OldFormWindow) Step(named, holding []string, now time.Time) (next OldFormWindow, closed bool, overdue []string) {
|
||||
@@ -146,7 +146,7 @@ const OldFormDescription = "The old reference form's window (design 51 *Every bu
|
||||
"the counted nodes, present or away, whose node-engine reads an older reference form alone and since when (a node holding it seven days " +
|
||||
"is the builder's finding " + OldFormHeld + "), when it last closed and who held it then, and the retired nodes dropped from the count by " +
|
||||
"hand. With drop (a node) and why, drops that retired node from the count by hand, a hand act the builder answers; a node still placed " +
|
||||
"leaves the count by itself only when the placement no longer names it. builder_engine_reads shows what each node's engine states."
|
||||
"leaves the count by itself only when the assignments no longer names it. builder_engine_reads shows what each node's engine states."
|
||||
|
||||
// AnswerOldForm is builder_old_form's read: the window as the state holds it.
|
||||
func AnswerOldForm(r Reader) (OldFormWindow, error) {
|
||||
|
||||
@@ -365,7 +365,7 @@ func TestTheBuildersVersions(t *testing.T) {
|
||||
t.Error("a drop with no why is taken")
|
||||
}
|
||||
// Security-upgrade F3 (review round 1), by mesh-sdk's window.StepWindow: a read naming no node (an empty
|
||||
// node_haves, the placement not read yet) never closes the window, and its holder's seven days still count.
|
||||
// node_haves, the assignments not read yet) never closes the window, and its holder's seven days still count.
|
||||
if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) {
|
||||
t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and
|
||||
// holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5).
|
||||
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains",
|
||||
"mesh-delivery-assigner.placement", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
"mesh-delivery-assigner.assignments", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
t.Errorf("reads = %v, want %v", m.Reads, want)
|
||||
}
|
||||
if want := []string{"node-build-go", "node-build-image", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
||||
@@ -111,18 +111,18 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no
|
||||
// other tool; it holds no capacity.
|
||||
// 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955).
|
||||
// DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's placement are asked by their
|
||||
// DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's assignments are asked by their
|
||||
// read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is
|
||||
// numbered under (design 32).
|
||||
for _, r := range m.Reads {
|
||||
// The toolchain gate follows the assigner's placement through meshmoduleassignerclient (buildrun.PlacementBucket), so
|
||||
// The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so
|
||||
// it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools.
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.placement") {
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.assignments") {
|
||||
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
|
||||
}
|
||||
}
|
||||
// Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements.
|
||||
if want := []string{"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
||||
"seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove",
|
||||
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
|
||||
@@ -137,7 +137,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed",
|
||||
"go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-catalog.manifest-accepted",
|
||||
"mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.placement", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**",
|
||||
"mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.assignments", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**",
|
||||
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "node-engine.manifest-reads-changed",
|
||||
"npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"oci-artifact-registry.published",
|
||||
"oci-artifact-registry.refused",
|
||||
"oci-artifact-registry.removed",
|
||||
"mesh-delivery-assigner.views.placement",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
@@ -72,7 +72,7 @@
|
||||
"seat:npm-package-registry.versions",
|
||||
"seat:oci-artifact-registry.remove",
|
||||
"seat:oci-artifact-registry.versions",
|
||||
"mesh-delivery-assigner.assigner_read_placement",
|
||||
"mesh-delivery-assigner.assigner_read_assignments",
|
||||
"mesh-delivery-indexer.indexer_read_candidates",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
@@ -103,7 +103,7 @@
|
||||
"mesh-build-image.steps",
|
||||
"mesh-build-node.steps",
|
||||
"mesh-build-node.toolchains",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-assigner.assignments",
|
||||
"mesh-delivery-gate.source"
|
||||
],
|
||||
"uses": [
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
|
||||
"mesh-delivery-checker/internal/check"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
@@ -58,7 +58,7 @@ func (e engineAsks) ComposeCandidate(ctx context.Context, node string, ask ne.Co
|
||||
}
|
||||
}
|
||||
|
||||
// placedNodes names every node the seat mesh-placement places now, read from its state when a check composes (no
|
||||
// placedNodes names every node the seat mesh-module-assigner places now, read from its state when a check composes (no
|
||||
// watch: a check reads it once). A state that cannot be read is an error, so the check says its composition was not
|
||||
// made rather than passing with no node asked (review finding 4).
|
||||
type placedNodes struct {
|
||||
@@ -70,14 +70,14 @@ func (p placedNodes) Placed() ([]string, error) {
|
||||
defer cancel()
|
||||
kv, err := p.kv(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.PlacementBucket, err)
|
||||
return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.AssignmentsBucket, err)
|
||||
}
|
||||
lister, err := kv.ListKeys(ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil // a placement that places nothing
|
||||
return nil, nil // assignments that places nothing
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.PlacementBucket, err)
|
||||
return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.AssignmentsBucket, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for key := range lister.Keys() {
|
||||
|
||||
@@ -25,13 +25,13 @@ import (
|
||||
"mesh-delivery-checker/internal/check"
|
||||
|
||||
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
@@ -323,13 +323,13 @@ func (b *bed) said(t *testing.T, words string) {
|
||||
// the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked.
|
||||
func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput {
|
||||
t.Helper()
|
||||
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.PlacementBucket, History: 1})
|
||||
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.AssignmentsBucket, History: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range nodes {
|
||||
pk, _ := meshmoduleassignerclient.NodeKey(n)
|
||||
raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.PlacementByNodeNodeValue{Node: n})
|
||||
raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.AssignmentsByNodeNodeValue{Node: n})
|
||||
if _, err := kv.Put(b.ctx, pk, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -466,7 +466,7 @@ func startBed(t *testing.T) *bed {
|
||||
for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} {
|
||||
viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule)
|
||||
}
|
||||
viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StatePlacement, buildrun.AssignerModule+".assigner_read_placement", "n1_"+check.CheckerModule)
|
||||
viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StateAssignments, buildrun.AssignerModule+".assigner_read_assignments", "n1_"+check.CheckerModule)
|
||||
// 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone
|
||||
// judges the repository (an unknown repository is judged by both, issue 1887 A12).
|
||||
known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"})
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
"mesh-delivery-checker/internal/check"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
@@ -145,7 +145,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
logger.Printf("what the checker keeps could not be said on the bus, so readers of owed, previews and verdicts are not granted until it is: %v", err)
|
||||
}
|
||||
// The candidates it checks, the modules and manifests that say which build modules judge a repository, the published
|
||||
// packages, the predefined keys and the placement are their owners' states (the indexer's, the assigner's), read by the
|
||||
// packages, the predefined keys and the assignments are their owners' states (the indexer's, the assigner's), read by the
|
||||
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2).
|
||||
// An owner that is down never holds the start: the checks start once every first view is answered, and until then the
|
||||
// unanswered first view is the checker's finding.
|
||||
@@ -168,7 +168,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
manifests := followOwner(check.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews)
|
||||
packages := followOwner(check.IndexerModule, meshcatalog.StatePackages, "indexer_read_packages", indexerViews)
|
||||
predefined := followOwner(check.IndexerModule, check.IndexerPredefinedState, "indexer_read_predefined", indexerViews)
|
||||
placement := followOwner(buildrun.AssignerModule, meshmoduleassigner.StatePlacement, "assigner_read_placement", assignerViews)
|
||||
assignments := followOwner(buildrun.AssignerModule, meshmoduleassigner.StateAssignments, "assigner_read_assignments", assignerViews)
|
||||
// The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails.
|
||||
seatDefs := followOwner(check.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews)
|
||||
if followed != nil {
|
||||
@@ -246,7 +246,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
verdicts := &manifestVerdicts{}
|
||||
compositions := check.EngineCompositions{
|
||||
Asker: engineAsks{call: caller.CallOn},
|
||||
Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return placement, nil }}.Placed,
|
||||
Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return assignments, nil }}.Placed,
|
||||
Bound: answerBound,
|
||||
Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}},
|
||||
Verdicts: verdicts.all,
|
||||
@@ -303,23 +303,23 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// The toolchains first, so a check that starts from the first candidate finds the nodes.
|
||||
watch := newToolchainWatch(ctx, js, tools, ch.NodesChanged, logger)
|
||||
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
|
||||
// placement is settled first, so the toolchains settle through the gate already knowing it.
|
||||
// assignments is settled first, so the toolchains settle through the gate already knowing it.
|
||||
gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf)
|
||||
watch.put = gate.Toolchain
|
||||
// Until the placement's first view is answered the gate passes every report on, as before the placement is settled.
|
||||
placementErrs := make(chan error, 1)
|
||||
// Until the assignments's first view is answered the gate passes every report on, as before the assignments is settled.
|
||||
assignmentsErrs := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-placement.Follower().Ready():
|
||||
case <-assignments.Follower().Ready():
|
||||
}
|
||||
pe, err := buildrun.FollowPlacement(ctx, reader, gate, ch.NodesChanged, logger.Printf)
|
||||
pe, err := buildrun.FollowAssignments(ctx, reader, gate, ch.NodesChanged, logger.Printf)
|
||||
if err != nil {
|
||||
placementErrs <- err
|
||||
assignmentsErrs <- err
|
||||
return
|
||||
}
|
||||
placementErrs <- <-pe
|
||||
assignmentsErrs <- <-pe
|
||||
}()
|
||||
if err := watch.start(); err != nil {
|
||||
return err
|
||||
@@ -481,7 +481,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
defer hub.Stop()
|
||||
// The checks, the tries and the hand asks, once every first view is answered (the lead's ruling for DLV-M2): a check
|
||||
// judged without the candidates, the modules, the manifests, the published packages, the predefined keys or the
|
||||
// placement would be another check.
|
||||
// assignments would be another check.
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -500,7 +500,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return nil
|
||||
case err := <-errs:
|
||||
return err
|
||||
case err := <-placementErrs:
|
||||
case err := <-assignmentsErrs:
|
||||
return err
|
||||
case err := <-watch.errs:
|
||||
return err
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// 1945 (a part exits at start for a bucket it works without): a bus that refuses every toolchains bucket leaves the
|
||||
// watch unstarted and the part running, said; a refused bucket it cannot work without is an error naming the bucket and
|
||||
// its grant. The gate over the seat's placement, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
||||
// its grant. The gate over the seat's assignments, and its refusal, are mesh-sdk's (buildrun.ToolchainGate,
|
||||
// issue 774).
|
||||
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
|
||||
set := &buildrun.ToolchainSet{}
|
||||
|
||||
@@ -5,9 +5,9 @@ go 1.26.0
|
||||
require (
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -3,9 +3,9 @@ package check
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
)
|
||||
|
||||
// withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine.
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
)
|
||||
@@ -46,7 +46,7 @@ type Sandbox interface {
|
||||
// EngineCompositions is the checker's composition of a check (Examiner.Compositions).
|
||||
type EngineCompositions struct {
|
||||
Asker EngineAsker
|
||||
// Placed names every node in placement now (the seat mesh-placement's state), or why it cannot be read: then no node
|
||||
// Placed names every node in assignments now (the seat mesh-module-assigner's state), or why it cannot be read: then no node
|
||||
// can be asked, and the composition is not made (an error), never passed with nobody asked.
|
||||
Placed func() ([]string, error)
|
||||
// Bound is the answer bound (the setting compose-answer-bound); zero is nec.AnswerBound.
|
||||
@@ -80,7 +80,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.Com
|
||||
var nodes []string
|
||||
if e.Placed != nil {
|
||||
if nodes, err = e.Placed(); err != nil {
|
||||
return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err)
|
||||
return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the assignments cannot be read, so no node can be asked: %w", err)
|
||||
}
|
||||
}
|
||||
bound := e.Bound
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
)
|
||||
@@ -204,11 +204,11 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Review finding 4: a placement that cannot be read is no mesh with no node: the composition is not made, said.
|
||||
t.Run("an unreadable placement makes no composition", func(t *testing.T) {
|
||||
// Review finding 4: assignments that cannot be read is no mesh with no node: the composition is not made, said.
|
||||
t.Run("an unreadable assignments makes no composition", func(t *testing.T) {
|
||||
ec := EngineCompositions{Asker: &fakeEngines{}, Placed: func() ([]string, error) { return nil, errors.New("no such bucket") }}
|
||||
if _, err := ec.Compose(context.Background(), cs); err == nil || !strings.Contains(err.Error(), "no such bucket") {
|
||||
t.Errorf("an unreadable placement: %v", err)
|
||||
t.Errorf("an unreadable assignments: %v", err)
|
||||
}
|
||||
b := newBench(t)
|
||||
b.ex.Compositions = ec
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
|
||||
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-checker/checkermsg"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
t.Errorf("reads = %v, want %v", m.Reads, want)
|
||||
}
|
||||
// DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the
|
||||
// assigner's placement are asked by their read tools and followed by their views families, never read.
|
||||
// assigner's assignments are asked by their read tools and followed by their views families, never read.
|
||||
for _, r := range m.Reads {
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
|
||||
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
|
||||
@@ -126,7 +126,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
// The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb
|
||||
// compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs.
|
||||
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
|
||||
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_seat_definitions",
|
||||
"seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
||||
@@ -138,7 +138,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
|
||||
// And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*).
|
||||
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
|
||||
"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.assignments", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-build-go.toolchain.reported",
|
||||
"mesh-build-node.toolchain.reported",
|
||||
"mesh-delivery-assigner.views.placement",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
@@ -77,7 +77,7 @@
|
||||
"gitea.gitea_compare",
|
||||
"gitea.gitea_get_file",
|
||||
"gitea.gitea_list_branches",
|
||||
"mesh-delivery-assigner.assigner_read_placement",
|
||||
"mesh-delivery-assigner.assigner_read_assignments",
|
||||
"mesh-delivery-indexer.indexer_read_candidates",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
|
||||
@@ -143,11 +143,11 @@ type Forge struct {
|
||||
Act string `json:"act,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
Open []OpenPull `json:"open,omitempty"`
|
||||
// RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is
|
||||
// the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
|
||||
// RetiredAssignments is the assignments files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): assignments is
|
||||
// the seat mesh-module-assigner's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
|
||||
// refused, loudly, rather than read with them dropped.
|
||||
RetiredPlacements json.RawMessage `json:"placements,omitempty"`
|
||||
RetiredRead bool `json:"placements-read,omitempty"`
|
||||
RetiredAssignments json.RawMessage `json:"assignments,omitempty"`
|
||||
RetiredRead bool `json:"assignments-read,omitempty"`
|
||||
// Predefined is the catalogue's predefined.json, exact, at the merge commit of a merged message (novox/hq ADR 0565
|
||||
// §2): what the indexer keeps as its state `predefined`. PredefinedRead says the bridge read the merge and it holds
|
||||
// no predefined.json. Carried on a merged message only; an older bridge sends neither, and the indexer keeps what it
|
||||
@@ -226,9 +226,9 @@ func (f Forge) check() error {
|
||||
if len(f.Open) > 0 {
|
||||
return fmt.Errorf("delivery: only an open message names open pull requests")
|
||||
}
|
||||
if len(f.RetiredPlacements) > 0 || f.RetiredRead {
|
||||
return fmt.Errorf("delivery: a forge message carries placement files, which no repository holds since ADR 0567: " +
|
||||
"placement is the seat mesh-placement's state")
|
||||
if len(f.RetiredAssignments) > 0 || f.RetiredRead {
|
||||
return fmt.Errorf("delivery: a forge message carries assignments files, which no repository holds since ADR 0567: " +
|
||||
"assignments is the seat mesh-module-assigner's state")
|
||||
}
|
||||
if err := f.checkPredefined(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -148,8 +148,8 @@ func TestForge(t *testing.T) {
|
||||
{"empty lock hash", `"a"`, in(func(f *Forge) { f.Locks["a"]["go.sum"] = "" })},
|
||||
{"named paths outside trees", `"other"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"other": {"src": "o"}} })},
|
||||
{"a named path with no object", `"src"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"a": {"src": ""}} })},
|
||||
{"placement files", "mesh-placement", in(func(f *Forge) { f.RetiredPlacements = []byte(`{"anchor":"{}"}`) })},
|
||||
{"placements read", "mesh-placement", in(func(f *Forge) { f.RetiredRead = true })},
|
||||
{"assignments files", "mesh-module-assigner", in(func(f *Forge) { f.RetiredAssignments = []byte(`{"anchor":"{}"}`) })},
|
||||
{"assignments read", "mesh-module-assigner", in(func(f *Forge) { f.RetiredRead = true })},
|
||||
{"predefined not an object", "one JSON object", in(func(f *Forge) { f.Predefined = []byte(`["domain"]`) })},
|
||||
{"predefined both", "says the merge holds none", in(func(f *Forge) { f.Predefined = []byte(`{}`); f.PredefinedRead = true })},
|
||||
{"pull with the predefined list", "only a merged message", Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, PredefinedRead: true}},
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
|
||||
if len(f.Packages) != 0 || strings.Contains(string(msg.Data), `"packages"`) {
|
||||
t.Errorf("the merged message carries packages: %v", f.Packages)
|
||||
}
|
||||
// ADR 0567: placement is the seat mesh-placement's: a merge of the catalogue carries no placement file and asks the
|
||||
// ADR 0567: assignments is the seat mesh-module-assigner's: a merge of the catalogue carries no assignments file and asks the
|
||||
// forge for none.
|
||||
forge.calls = nil
|
||||
msg, err = Convert(mergedSubject, mergedBody(map[string]any{"repo": "mesh-catalog"}), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get)
|
||||
@@ -94,11 +94,11 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
|
||||
if f, err = forgemsg.DecodeForge(msg.Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.RetiredPlacements) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"placements`) {
|
||||
t.Errorf("a merge of the catalogue carries placement: %s", msg.Data)
|
||||
if len(f.RetiredAssignments) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"assignments`) {
|
||||
t.Errorf("a merge of the catalogue carries assignments: %s", msg.Data)
|
||||
}
|
||||
for _, c := range forge.calls {
|
||||
if strings.HasPrefix(c.path, "placement") {
|
||||
if strings.HasPrefix(c.path, "assignments") {
|
||||
t.Errorf("the forge was asked for %s", c.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func holdsTool(read stdio.KeptState, ask asker, now func() time.Time, acts meshh
|
||||
") which not every counted node-engine states, present or away (ADR 0624 §1). Per module: what it needs, which nodes " +
|
||||
"lack what (or why no node is counted yet), the manifest the modules state keeps and the one held, the merge that " +
|
||||
"brought it and since when. The hold moves by itself once every counted node-engine states it (on manifest-reads-changed, " +
|
||||
"the placement's change or a merge). With module and ask_again, the engines of the nodes that lack it are asked again " +
|
||||
"the assignments's change or a merge). With module and ask_again, the engines of the nodes that lack it are asked again " +
|
||||
"and the hold is weighed now; with module, release and why, it moves now by your word whatever the engines state (a hand " +
|
||||
"act, recorded through mesh-hand-acts, which names the nodes it strands: each node whose engine lacks the reading, an " +
|
||||
"away node from its return, reads the new entry without it and an engine before form:seat-reach finds no store). A hold " +
|
||||
|
||||
@@ -29,7 +29,7 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
|
||||
for _, t := range index.ReadTools {
|
||||
out = append(out, views.ReadTool(index.Module, t.Tool, t.State, t.Words, state(t.State), marks, now))
|
||||
}
|
||||
// Its own first views, of the assigner's placement (DLV-F3r): read, and asked again by hand.
|
||||
// Its own first views, of the assigner's assignments (DLV-F3r): read, and asked again by hand.
|
||||
return append(out, views.StatusTools(index.Module, "indexer_first_views", "indexer_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
@@ -559,7 +559,7 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
|
||||
b(ctx, js)
|
||||
}
|
||||
|
||||
serveAssignerPlacement(t, ctx, js, ctl)
|
||||
serveAssignerAssignments(t, ctx, js, ctl)
|
||||
|
||||
broker := filepath.Join(t.TempDir(), "broker")
|
||||
cred, _ := json.Marshal(process.Account{URL: srv.ClientURL(), Node: "n1", Module: "mesh-delivery-indexer", User: "n1.mesh-delivery-indexer", Password: "pw"})
|
||||
@@ -978,33 +978,33 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
|
||||
// TestIndexerCountsOnlyThePlacedBuildNodesToolchains (issue 1940, issue 774): the toolchains bucket holds the report of
|
||||
// n2, where the catalogue no longer places mesh-build-go, at another version than n1's, as ace's v26.12.0 stayed beside
|
||||
// the v26.12.1 of the nodes mesh-build-node runs on. The indexer gives the merged module a fingerprint from n1's
|
||||
// toolchain alone; when the placement puts mesh-build-go on n2 again, its report written since counts and the builders
|
||||
// toolchain alone; when the assignments puts mesh-build-go on n2 again, its report written since counts and the builders
|
||||
// disagree (the guard stays); when it is taken off again, the fingerprint comes back. No restart, no timer: the
|
||||
// placement's watch (ADR 0549 §3), not the controller's assignments.
|
||||
// assignments's watch (ADR 0549 §3), not the controller's assignments.
|
||||
func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
|
||||
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"
|
||||
var placement, toolchains jetstream.KeyValue
|
||||
var assignments, toolchains jetstream.KeyValue
|
||||
place := func(ctx context.Context, t *testing.T, node string, modules ...string) {
|
||||
p := meshmoduleassigner.Placement{Node: node}
|
||||
p := meshmoduleassigner.Assignments{Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
// Its node key, the commit that names the modules placed (research 072 row 37).
|
||||
values, err := meshmoduleassignerclient.PlacementValues(p)
|
||||
values, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := meshmoduleassigner.PlacementByNodeNodeKey(node)
|
||||
if _, err := placement.Put(ctx, key, values[key]); err != nil {
|
||||
key := meshmoduleassigner.AssignmentsByNodeNodeKey(node)
|
||||
if _, err := assignments.Put(ctx, key, values[key]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// DLV-F5r (ADR 0621 §3): the assigner's runtime starts together with the indexer and answers its first view only after
|
||||
// the indexer asked; within the bound the indexer follows the placement it answers (only n1's toolchain counts).
|
||||
// the indexer asked; within the bound the indexer follows the assignments it answers (only n1's toolchain counts).
|
||||
up := make(chan struct{})
|
||||
assignerLate = up
|
||||
t.Cleanup(func() { assignerLate = nil })
|
||||
@@ -1024,8 +1024,8 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// The seat mesh-placement's placement, as the assigner writes it (ADR 0567): n1 places mesh-build-go, n2 nothing.
|
||||
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil {
|
||||
// The seat mesh-module-assigner's assignments, as the assigner writes it (ADR 0567): n1 places mesh-build-go, n2 nothing.
|
||||
if assignments, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.AssignmentsBucket, History: index.AssignmentsHistory}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place(ctx, t, "n1", "mesh-build-go")
|
||||
@@ -1072,13 +1072,13 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
|
||||
if _, err := toolchains.Put(ctx, key, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// n2's report, written since its placement, counts: the builders disagree. A fingerprint already computed is kept
|
||||
// n2's report, written since its assignments, counts: the builders disagree. A fingerprint already computed is kept
|
||||
// (issue 1940), and only a new one needs the agreement: a merge moving avahi gets none until they agree.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if e, err := modules.Get(ctx, "avahi"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if m, err := meshcatalogclient.DecodeModule(e.Value()); err != nil || m.Fp2 != held.Fp2 {
|
||||
t.Fatalf("after n2's placement avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.Fp2, m.None2, held.Fp2)
|
||||
t.Fatalf("after n2's assignments avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.Fp2, m.None2, held.Fp2)
|
||||
}
|
||||
moved := strings.Replace(string(data), `"newtree"`, `"movedtree"`, 1)
|
||||
if moved == string(data) {
|
||||
@@ -1091,11 +1091,11 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
|
||||
return m.Fp2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2")
|
||||
}
|
||||
if m := read(disagree); !disagree(m) {
|
||||
t.Fatalf("within 20 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.Fp2, m.None2)
|
||||
t.Fatalf("within 20 s of a merge after n2's assignments avahi has fp2 %q, none2 %q: want the builders to disagree", m.Fp2, m.None2)
|
||||
}
|
||||
place(ctx, t, "n2")
|
||||
if m := read(agreed); !agreed(m) {
|
||||
t.Fatalf("within 20 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.Fp2, m.None2)
|
||||
t.Fatalf("within 20 s of n2's assignments taken away avahi has fp2 %q, none2 %q", m.Fp2, m.None2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,14 +1103,14 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
|
||||
// assigner's runtime started together with the indexer's, answering after the indexer asked (DLV-F5r).
|
||||
var assignerLate chan struct{}
|
||||
|
||||
// serveAssignerPlacement is the assigner as the owner of its placement (ADR 0615, ADR 0621), with the real owner's side:
|
||||
// its first-view tool placement answered from the bucket at its mark (the bucket made empty where a test made none),
|
||||
// serveAssignerAssignments is the assigner as the owner of its assignments (ADR 0615, ADR 0621), with the real owner's side:
|
||||
// its first-view tool assignments answered from the bucket at its mark (the bucket made empty where a test made none),
|
||||
// and its views family said by the views emitter onto ASSIGNER_VIEWS, which the indexer's durable consumer reads.
|
||||
func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
|
||||
func serveAssignerAssignments(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
|
||||
t.Helper()
|
||||
placement, err := js.KeyValue(ctx, index.PlacementBucket)
|
||||
assignments, err := js.KeyValue(ctx, index.AssignmentsBucket)
|
||||
if err != nil {
|
||||
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil {
|
||||
if assignments, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.AssignmentsBucket, History: index.AssignmentsHistory}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1124,11 +1124,11 @@ func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetS
|
||||
}
|
||||
em := views.Emitter{Owner: index.AssignerModule, Subject: process.Account{Node: "n1", Module: index.AssignerModule}.EventSubject,
|
||||
Publish: views.JetStreamPublish(js), Marks: marks}
|
||||
if err := em.Start(ctx, index.PlacementState, placement, nil); err != nil {
|
||||
if err := em.Start(ctx, index.AssignmentsState, assignments, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
late := assignerLate
|
||||
sub, err := nc.Subscribe("mesh.mod.mesh-delivery-assigner.call.placement.>", func(m *nats.Msg) {
|
||||
sub, err := nc.Subscribe("mesh.mod.mesh-delivery-assigner.call.assignments.>", func(m *nats.Msg) {
|
||||
if late != nil { // its runtime comes up after the indexer started (DLV-F5r)
|
||||
select {
|
||||
case <-late:
|
||||
@@ -1136,7 +1136,7 @@ func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetS
|
||||
return
|
||||
}
|
||||
}
|
||||
a, err := views.FirstView(index.AssignerModule, index.PlacementState, jsSource{ctx, placement}, jsSource{ctx, marks}, "", time.Now())
|
||||
a, err := views.FirstView(index.AssignerModule, index.AssignmentsState, jsSource{ctx, assignments}, jsSource{ctx, marks}, "", time.Now())
|
||||
out := map[string]any{"result": a, "node": "n0"}
|
||||
if err != nil {
|
||||
out = map[string]any{"error": err.Error()}
|
||||
|
||||
@@ -472,10 +472,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
}
|
||||
// Only the toolchain reports of nodes the catalogue places the build module on count (issue 1940, ADR 0549 §3): the
|
||||
// placement is settled first, so the toolchains settle through the gate already knowing it.
|
||||
// assignments is settled first, so the toolchains settle through the gate already knowing it.
|
||||
gate := buildrun.NewToolchainGate(sortedModules(), ix.PutToolchain, logger.Printf)
|
||||
// What each counted node's engine reads (ADR 0624 §1), for the mesh-wide hold of a manifest every node reads (ADR 0632
|
||||
// §2): asked through the node-engine seat's verb as the indexer's own account, for every node the placement names,
|
||||
// §2): asked through the node-engine seat's verb as the indexer's own account, for every node the assignments names,
|
||||
// kept in the state engine-reads once it is bound; each change weighs the holds again.
|
||||
reads := newEngineReads(calls.CallOn, gate, func() { ix.KickHolds(ctx) }, logger.Printf)
|
||||
gate.OnPlaced = reads.placed
|
||||
@@ -506,14 +506,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
process.AwaitBucket(ctx, js, conn, index.EngineReadsBucket, "what each node's engine reads is kept in memory only", logger.Printf).Ready(func(kv jetstream.KeyValue) {
|
||||
reads.bind(ctx, kv, errs, logger.Printf)
|
||||
})
|
||||
// The placement is the assigner's: read through its first view and its views family, never its bucket (DLV-F1r).
|
||||
placementErrs := make(chan error)
|
||||
// The assignments is the assigner's: read through its first view and its views family, never its bucket (DLV-F1r).
|
||||
assignmentsErrs := make(chan error)
|
||||
// The indexer's one pull of its durable on the shared events stream (process.EventHub): every handler is registered
|
||||
// before it starts.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
followPlacement(ctx, hub, cred, calls.Call, gate, func(ctx context.Context) {
|
||||
followAssignments(ctx, hub, cred, calls.Call, gate, func(ctx context.Context) {
|
||||
if err := ix.Recompute(ctx); err != nil {
|
||||
logger.Printf("writing the modules again after the placement of a build module changed: %v", err)
|
||||
logger.Printf("writing the modules again after the assignments of a build module changed: %v", err)
|
||||
}
|
||||
}, findings, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "indexer_first_views reads nothing and indexer_first_view_again is not heard", logger.Printf), logger.Printf)
|
||||
tools := newToolchainWatch(ctx, js, toolchainSink{
|
||||
@@ -649,7 +649,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
if err := hub.Handle(numberedSubject, versions); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = notHeard.Bind(process.EventsStream, "no package's series moves, no node's manifest-reads-changed and no placement view is heard until the indexer starts again", func() error {
|
||||
_ = notHeard.Bind(process.EventsStream, "no package's series moves, no node's manifest-reads-changed and no assignments view is heard until the indexer starts again", func() error {
|
||||
if err := hub.Start(ctx, js, cred); err != nil {
|
||||
stopped = cmp.Or(stopped, err)
|
||||
return err
|
||||
@@ -716,7 +716,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
case err := <-sourceErrs:
|
||||
return err
|
||||
case err := <-placementErrs:
|
||||
case err := <-assignmentsErrs:
|
||||
return err
|
||||
case err := <-tools.errs:
|
||||
return err
|
||||
|
||||
@@ -9,23 +9,23 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// engineReads is what each counted node's engine states it reads, for the indexer's mesh-wide hold (index.Holds; novox/hq
|
||||
// ADR 0632 §2, ADR 0624 §1), in the shape the builder keeps it: asked through the node-engine seat's verb manifest-reads
|
||||
// for every node the placement names, on the indexer's start, on the seat's event manifest-reads-changed for that node and
|
||||
// for every node the assignments names, on the indexer's start, on the seat's event manifest-reads-changed for that node and
|
||||
// when a person asks again (indexer_engine_reads, indexer_holds); kept in the indexer's state engine-reads, one key per
|
||||
// node, so a node that is away keeps its last statement. A node the placement no longer names is forgotten. Until the
|
||||
// placement is read no node is counted, and every hold stays.
|
||||
// node, so a node that is away keeps its last statement. A node the assignments no longer names is forgotten. Until the
|
||||
// assignments is read no node is counted, and every hold stays.
|
||||
type engineReads struct {
|
||||
keeper *enginereads.Keeper
|
||||
gate *buildrun.ToolchainGate
|
||||
mu sync.Mutex
|
||||
known bool // the placement is read
|
||||
known bool // the assignments is read
|
||||
nodes []string
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (r *engineReads) bind(ctx context.Context, kv jetstream.KeyValue, errs chan
|
||||
r.ask(nodes, "the state engine-reads is bound")
|
||||
}
|
||||
|
||||
// placed takes the placement in, once it is read and after each change of it: a node it names newly is asked, one it no
|
||||
// placed takes the assignments in, once it is read and after each change of it: a node it names newly is asked, one it no
|
||||
// longer names is forgotten, key and all.
|
||||
func (r *engineReads) placed() {
|
||||
nodes := r.gate.Nodes()
|
||||
@@ -64,7 +64,7 @@ func (r *engineReads) placed() {
|
||||
r.mu.Unlock()
|
||||
for _, node := range nodes {
|
||||
if first || !slices.Contains(before, node) {
|
||||
r.keeper.AskNode(node, "counted: the placement names it")
|
||||
r.keeper.AskNode(node, "counted: the assignments names it")
|
||||
}
|
||||
}
|
||||
for _, node := range before {
|
||||
@@ -94,7 +94,7 @@ func (r *engineReads) heard(m jetstream.Msg, logf func(string, ...any)) bool {
|
||||
}
|
||||
|
||||
// counted is index.Indexer.Counted: what each counted node states (nil, the baseline, for one with no statement), and
|
||||
// whether the placement is read.
|
||||
// whether the assignments is read.
|
||||
func (r *engineReads) counted() (map[string][]string, bool) {
|
||||
r.mu.Lock()
|
||||
nodes, known := slices.Clone(r.nodes), r.known
|
||||
|
||||
@@ -13,15 +13,15 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// findingsBucket is the indexer's own findings, which the condition keeper raises and clears (ADR 0564 §2): a value too
|
||||
// large for its views event, and its first view of the assigner's placement unanswered past its bound.
|
||||
// large for its views event, and its first view of the assigner's assignments unanswered past its bound.
|
||||
var findingsBucket = mcc.FindingsBucket(index.Module)
|
||||
|
||||
// readyOf binds a state's bucket: now for one bound at the start, once it appears for one awaited.
|
||||
@@ -109,15 +109,15 @@ const graphState = graph.State
|
||||
// where indexer_first_view_again asks.
|
||||
var firstViewsBucket = index.Module + "_" + views.StatusState
|
||||
|
||||
// placementTool is the assigner's first-view tool of its placement, as the indexer's invokes names it (ADR 0621 §2).
|
||||
const placementTool = index.AssignerModule + "." + index.PlacementState
|
||||
// assignmentsTool is the assigner's first-view tool of its assignments, as the indexer's invokes names it (ADR 0621 §2).
|
||||
const assignmentsTool = index.AssignerModule + "." + index.AssignmentsState
|
||||
|
||||
// followPlacement reads the seat mesh-placement's placement through the assigner's first view and then its views
|
||||
// followAssignments reads the seat mesh-module-assigner's assignments through the assigner's first view and then its views
|
||||
// family, never its bucket (ADR 0615 §1, DLV-F1r), into gate: each key as it changes, settled once answered, with
|
||||
// changed told whenever what the gate passes on changed. It waits for the first answer at most views.Bound, so a
|
||||
// normal start, the two owners coming up together, computes the fingerprints once; past it the indexer goes on, the gate
|
||||
// passing every report, and raises its condition naming the assigner and placement until the answer clears it.
|
||||
func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Account, call func(string, any) (json.RawMessage, error),
|
||||
// passing every report, and raises its condition naming the assigner and assignments until the answer clears it.
|
||||
func followAssignments(ctx context.Context, hub *process.EventHub, cred process.Account, call func(string, any) (json.RawMessage, error),
|
||||
gate *buildrun.ToolchainGate, changed func(context.Context), findings, status *process.AwaitedBucket, logf func(string, ...any)) *views.Follower {
|
||||
finding := func(s views.Status, raised bool) {
|
||||
w := sdkfindings.Writer(ctx, findings.KV)
|
||||
@@ -133,11 +133,11 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
|
||||
}
|
||||
}
|
||||
var settled atomic.Bool // the first view answered: the gate's nodes are the counted ones from now on
|
||||
f := &views.Follower{Owner: index.AssignerModule, Tool: placementTool, State: index.PlacementState,
|
||||
Ask: views.AskVia(call, placementTool), Logf: logf,
|
||||
f := &views.Follower{Owner: index.AssignerModule, Tool: assignmentsTool, State: index.AssignmentsState,
|
||||
Ask: views.AskVia(call, assignmentsTool), Logf: logf,
|
||||
Raise: func(s views.Status) { finding(s, true) }, Clear: func(s views.Status) { finding(s, false) },
|
||||
Changed: func(key string, value json.RawMessage, deleted bool, at time.Time) {
|
||||
if gate.Placement(key, value, deleted, at) {
|
||||
if gate.Assignments(key, value, deleted, at) {
|
||||
changed(ctx)
|
||||
}
|
||||
if settled.Load() && gate.OnPlaced != nil {
|
||||
@@ -160,21 +160,21 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
|
||||
}
|
||||
settled.Store(true)
|
||||
if gate.OnPlaced != nil {
|
||||
gate.OnPlaced() // the placement is read: its nodes are the counted ones
|
||||
gate.OnPlaced() // the assignments is read: its nodes are the counted ones
|
||||
}
|
||||
close(answered)
|
||||
}()
|
||||
// The assigner's views family, on the module's one hub (the host starts it once every handler is registered); a family the
|
||||
// hub cannot register is the finding family-not-heard until the indexer starts again (ADR 0615 §5).
|
||||
_ = notHeardWriter(ctx, findings, logf).Bind(process.EventsStream, "the placement is the first view's alone, asked again by hand, until the indexer starts again", func() error {
|
||||
return hub.Handle("mesh.mod."+index.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{index.PlacementState: f}, logf))
|
||||
_ = notHeardWriter(ctx, findings, logf).Bind(process.EventsStream, "the assignments is the first view's alone, asked again by hand, until the indexer starts again", func() error {
|
||||
return hub.Handle("mesh.mod."+index.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{index.AssignmentsState: f}, logf))
|
||||
})
|
||||
wait, cancel := context.WithTimeout(ctx, views.Bound) // a context's bound, no timer (ADR 0437 §8)
|
||||
defer cancel()
|
||||
select {
|
||||
case <-answered:
|
||||
case <-wait.Done():
|
||||
logf("the assigner has not answered placement within %s: the indexer goes on, every node's toolchain report counting until it answers", views.Bound)
|
||||
logf("the assigner has not answered assignments within %s: the indexer goes on, every node's toolchain report counting until it answers", views.Bound)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ go 1.26.0
|
||||
require (
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -129,7 +129,7 @@ type Indexer struct {
|
||||
// capability read mesh-wide, waiting for every counted node-engine to state it. Nil, or Counted nil, holds nothing.
|
||||
Holds Store
|
||||
// Counted is what each counted node's engine states it reads (ADR 0624 §1; nil, the baseline, for a node with no
|
||||
// statement), and whether the counted nodes are known (the placement is read).
|
||||
// statement), and whether the counted nodes are known (the assignments is read).
|
||||
Counted func() (map[string][]string, bool)
|
||||
// AskEngines asks these nodes' engines what they read again (the node-engine seat's verb manifest-reads); nil asks none.
|
||||
AskEngines func(nodes []string, why string)
|
||||
|
||||
@@ -28,10 +28,10 @@ import (
|
||||
// it is written as its merge would have written it, judged and said. The assigner's hold of ADR 0614 §4 holds a want on
|
||||
// one node; this one holds the entry itself, which every node reads.
|
||||
//
|
||||
// "Counted" is ADR 0624 §1's set: every node the placement names, present or away, each read as its engine last stated
|
||||
// "Counted" is ADR 0624 §1's set: every node the assignments names, present or away, each read as its engine last stated
|
||||
// through the node-engine seat's verb manifest-reads (a node with no statement read as the baseline; the indexer's state
|
||||
// engine-reads, nodeengineclient/enginereads). Until the placement is read no node is counted and every hold stays. The hold moves
|
||||
// on the event manifest-reads-changed, the placement's change, a person's ask again, and a merge; never on a timer.
|
||||
// engine-reads, nodeengineclient/enginereads). Until the assignments is read no node is counted and every hold stays. The hold moves
|
||||
// on the event manifest-reads-changed, the assignments's change, a person's ask again, and a merge; never on a timer.
|
||||
const (
|
||||
// HoldsState is the indexer's state of held entries: the bucket mesh-delivery-indexer_holds, keyed as the modules state.
|
||||
HoldsState = "holds"
|
||||
@@ -126,7 +126,7 @@ func meshWide(needs []string) []string {
|
||||
}
|
||||
|
||||
// noneCounted is why a hold stays while no node is counted.
|
||||
const noneCounted = "no node is counted yet (the placement is not read), so the hold stays until it is"
|
||||
const noneCounted = "no node is counted yet (the assignments is not read), so the hold stays until it is"
|
||||
|
||||
// lacking is, of the counted nodes, each whose engine lacks some of needs, with what it lacks; or why no node is counted.
|
||||
func (ix *Indexer) lacking(needs []string) (map[string][]string, string) {
|
||||
@@ -232,7 +232,7 @@ func (ix *Indexer) dropHold(ctx context.Context, key, why string) error {
|
||||
}
|
||||
|
||||
// KickHolds moves every hold whose needs every counted node-engine now states, in the background, once at a time: on a
|
||||
// node's statement changed, the placement's change and the holds state's binding.
|
||||
// node's statement changed, the assignments's change and the holds state's binding.
|
||||
func (ix *Indexer) KickHolds(ctx context.Context) {
|
||||
if ix.Holds == nil || ix.Counted == nil {
|
||||
return
|
||||
|
||||
@@ -37,9 +37,9 @@ const (
|
||||
CheckerModule = "mesh-delivery-checker"
|
||||
BuildGoModule = "mesh-build-go"
|
||||
BuildNodeModule = "mesh-build-node"
|
||||
PlacementState = "placement"
|
||||
PlacementBucket = AssignerModule + "_" + PlacementState
|
||||
PlacementHistory = 5
|
||||
AssignmentsState = "assignments"
|
||||
AssignmentsBucket = AssignerModule + "_" + AssignmentsState
|
||||
AssignmentsHistory = 5
|
||||
SourceBucket = GateModule + "_source"
|
||||
PreviewsBucket = CheckerModule + "_previews"
|
||||
PreviewsHistory = 5
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
var OldSections = []string{"state", "logs", "streams", "objects"}
|
||||
|
||||
// Foundation are the modules that keep using the old sections for good: exactly the nine parts the bus module's own
|
||||
// composition, placement, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the
|
||||
// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3): the bus module, the
|
||||
// grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config and the vault. Their new
|
||||
// state goes on their old sections too, so a foundation manifest asking bus state in the new form is refused.
|
||||
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
if m.Module != "mesh-delivery-indexer" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says
|
||||
// 1126: assignments is the seat mesh-module-assigner's (ADR 0567): the indexer holds no assignments state. ADR 0578 §1: it says
|
||||
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
|
||||
// manifest-accepted, both events of the seat mesh-catalog it holds (ADR 0636 §4), so not among its `emits`.
|
||||
if own := slices.DeleteFunc(slices.Clone(m.Emits), func(e string) bool { return strings.HasPrefix(e, views.EventPrefix) }); !reflect.DeepEqual(own, []string{takein.TakeInPendingEvent, takein.TakeInTakenEvent}) {
|
||||
@@ -136,14 +136,14 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
t.Errorf("the indexer reads the bucket %s, its manifest's reads are %v (want %s)", bucket, m.Reads, read)
|
||||
}
|
||||
}
|
||||
// DLV-F1r (ADR 0615 §1): the assigner's placement is read through its first view and views family, never its state.
|
||||
// DLV-F1r (ADR 0615 §1): the assigner's assignments is read through its first view and views family, never its state.
|
||||
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
||||
t.Errorf("reads = %v, want %v", m.Reads, want)
|
||||
}
|
||||
// "Reads FORGE" is a consumes of the indexer's stream: the broker grants only buckets by reads (mesh-control
|
||||
// internal/broker/state.go:62-93).
|
||||
// The registry seats' published and the builder's numbered fill each package's series (DLV-I5, design 51 *The versions*).
|
||||
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.placement",
|
||||
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-builder.version.numbered", "mesh-delivery-checker.preview.written", "mesh-delivery-checker.try.ended", "mesh-delivery-checker.try.started", "mesh-delivery-checker.verdict", "mesh-delivery-forge-bridge.forge.**",
|
||||
"node-engine.manifest-reads-changed", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { // ADR 0632 §2: the counted engines' reads
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
@@ -157,7 +157,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
|
||||
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
|
||||
// commit is in another (issue 1900).
|
||||
if want := []string{"mesh-delivery-assigner.placement", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"go-package-registry.published",
|
||||
"mesh-build-go.toolchain.reported",
|
||||
"mesh-build-node.toolchain.reported",
|
||||
"mesh-delivery-assigner.views.placement",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-builder.version.numbered",
|
||||
"mesh-delivery-checker.preview.written",
|
||||
"mesh-delivery-checker.try.ended",
|
||||
@@ -119,7 +119,7 @@
|
||||
"mesh-delivery-gate.source"
|
||||
],
|
||||
"invokes": [
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-assigner.assignments",
|
||||
"gitea.gitea_commit_trees",
|
||||
"gitea.gitea_contains",
|
||||
"gitea.gitea_get_file",
|
||||
|
||||
@@ -40,8 +40,8 @@ require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// how a module is spoken to"). An ask the teller holds open at the router keeps one of the three places the router gives
|
||||
// its asker (asks.MostOpen) until it is answered, it lapses, or the gate or the assigner takes its key away. When that last
|
||||
// never comes (on 2026-10-31 two merge asks of pull requests merged by another path held two places for a day, and a
|
||||
// placement ask was refused for want of room), a person ends the ask by hand. The teller's process serves no request
|
||||
// assignments ask was refused for want of room), a person ends the ask by hand. The teller's process serves no request
|
||||
// subject (ADR 0437 §4), so the tool writes an end into the teller's state ends, which the teller watches, carries out
|
||||
// and answers there, as the indexer answers indexer_take_in.
|
||||
package ends
|
||||
|
||||
@@ -4,20 +4,22 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"mesh-delivery-view/internal/view"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
@@ -27,11 +29,13 @@ import (
|
||||
func serve(t *testing.T) string {
|
||||
t.Helper()
|
||||
m := view.New()
|
||||
placed, err := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}})
|
||||
placed, err := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Apply(view.Entry{Bucket: view.PlacementBucket, Key: "placement.a", Value: placed})
|
||||
for _, k := range slices.Sorted(maps.Keys(placed)) { // a key per module, then the node key
|
||||
m.Apply(view.Entry{Bucket: view.AssignmentsBucket, Key: k, Value: placed[k]})
|
||||
}
|
||||
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/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/modules/mesh-delivery-indexer/takein"
|
||||
|
||||
@@ -6,23 +6,25 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"maps"
|
||||
"mesh-delivery-view/internal/view"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
@@ -70,7 +72,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
kvs := map[string]jetstream.KeyValue{}
|
||||
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -90,20 +92,22 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
|
||||
}
|
||||
return b
|
||||
}
|
||||
// The seat's placement (issue 774, ADR 0549 §3): x and y on a, nothing on b.
|
||||
placeValue := func(node string, modules ...string) []byte {
|
||||
p := meshmoduleassigner.Placement{Node: node}
|
||||
// The seat's assignments (issue 774, ADR 0549 §3): x and y on a, nothing on b.
|
||||
place := func(node string, modules ...string) {
|
||||
p := meshmoduleassigner.Assignments{Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
v, err := meshmoduleassignerclient.EncodePlacement(p)
|
||||
values, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
for _, k := range slices.Sorted(maps.Keys(values)) { // a key per module, then the node key
|
||||
put(view.AssignmentsBucket, k, values[k])
|
||||
}
|
||||
}
|
||||
put(view.PlacementBucket, "placement.a", placeValue("a", "x", "y"))
|
||||
put(view.PlacementBucket, "placement.b", placeValue("b"))
|
||||
place("a", "x", "y")
|
||||
place("b")
|
||||
put(view.HaveBucket, "a.x", haveValue("b1", nodeengineclient.PhaseChecking, nodeengineclient.HealthWaits))
|
||||
put(view.HaveBucket, "b.x", haveValue("b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK))
|
||||
|
||||
@@ -182,7 +186,7 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
kvs := map[string]jetstream.KeyValue{}
|
||||
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -269,7 +273,7 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
for _, b := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, the seat's placement
|
||||
// Command mesh-delivery-view is the operator's view of delivery (novox/hq ADR 0429 §3): it watches the have, the seat's assignments
|
||||
// and builds buckets on its own bus credential, derives one row per module and node in memory, and serves them on a
|
||||
// local socket (for mesh-delivery-cli) and on a local web page. It starts, stops or retries nothing; the one thing it writes is the operator's answer to an ask that needs no proof of who
|
||||
// answered, under `<ask key>.view` in its bucket answers (ADR 0528 clause 6).
|
||||
@@ -21,9 +21,9 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
@@ -126,7 +126,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
}
|
||||
open := buckets{js: js, reader: reader}
|
||||
for _, bucket := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
|
||||
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
|
||||
go follow(ctx, open, bucket, false, model, logger)
|
||||
}
|
||||
// The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556).
|
||||
@@ -185,7 +185,7 @@ var (
|
||||
// owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the
|
||||
// view's tools ask the other six through the owners' read tools on each call.
|
||||
var ownersFollowed = []struct{ owner, state, tool string }{
|
||||
{view.AssignerModule, view.PlacementState, view.AssignerModule + ".assigner_read_placement"},
|
||||
{view.AssignerModule, view.AssignmentsState, view.AssignerModule + ".assigner_read_assignments"},
|
||||
{view.AssignerModule, "wants", view.AssignerModule + ".assigner_read_wants"},
|
||||
{view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"},
|
||||
{view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"},
|
||||
|
||||
@@ -7,9 +7,9 @@ require (
|
||||
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/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/clients/meshwantsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
@@ -58,7 +58,7 @@ require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package view
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/motion"
|
||||
)
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ const (
|
||||
IndexerConfigCandidatesState = "config-candidates"
|
||||
|
||||
// The assigner's states.
|
||||
PlacementState = "placement"
|
||||
PlacementBucket = AssignerModule + "_" + PlacementState
|
||||
AssignmentsState = "assignments"
|
||||
AssignmentsBucket = AssignerModule + "_" + AssignmentsState
|
||||
AssignerWantsBucket = AssignerModule + "_wants"
|
||||
AssignerAsksBucket = AssignerModule + "_asks"
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user