From 3fd09d50e98a67c07a5a02b863a0346ed91645dc Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:01:10 +0100 Subject: [PATCH] =?UTF-8?q?Assigner:=20a=20want,=20a=20node's=20own=20want?= =?UTF-8?q?=20and=20a=20hold=20are=20the=20wants=20client's,=20a=20put=20i?= =?UTF-8?q?s=20the=20assigner's=20own=20puts=20package=20(ADR=200634=20?= =?UTF-8?q?=C2=A74,=20=C2=A75;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../integration_test.go | 106 +++--- .../cmd/mesh-delivery-assigner/pass.go | 71 ++-- .../cmd/mesh-delivery-assigner/pass_test.go | 106 +++--- .../cmd/mesh-delivery-assigner/puts.go | 56 +-- .../cmd/mesh-delivery-assigner/tools.go | 31 +- .../cmd/mesh-delivery-assigner/wants_tools.go | 7 +- .../cmd/mesh-delivery-assigner/world.go | 11 +- modules/mesh-delivery-assigner/go.mod | 2 + .../internal/assign/asks.go | 5 +- .../internal/assign/asks_test.go | 8 +- .../internal/assign/put.go | 61 ++-- .../internal/assign/wants.go | 113 +++--- .../internal/assign/wants_test.go | 341 +++++++++--------- .../internal/puts/puts.go | 156 ++++++++ .../internal/puts/puts_test.go | 66 ++++ 15 files changed, 690 insertions(+), 450 deletions(-) create mode 100644 modules/mesh-delivery-assigner/internal/puts/puts.go create mode 100644 modules/mesh-delivery-assigner/internal/puts/puts_test.go diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 8970088e..1ab80af8 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" @@ -17,6 +18,7 @@ import ( "io" "maps" "mesh-delivery-assigner/internal/buckets" + "mesh-delivery-assigner/internal/puts" "os" "path/filepath" "slices" @@ -247,14 +249,14 @@ func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) { t.Fatalf("without %s the assigner stopped: %v", buckets.Puts, err) case <-time.After(2 * time.Second): } - puts, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buckets.Puts, History: 5}) + putsKV, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buckets.Puts, History: 5}) if err != nil { t.Fatal(err) } - pkey, _ := delivery.PutKey("n1", "web") - asked := delivery.Put{FP: meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64), Why: "a try", At: time.Now()}.Said(time.Now(), delivery.PutAsked, "asked") - raw, _ := delivery.EncodePut(asked) - if _, err := puts.Put(b.ctx, pkey, raw); err != nil { + pkey, _ := puts.Key("n1", "web") + asked := puts.Put{FP: meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64), Why: "a try", At: time.Now()}.Said(time.Now(), puts.Asked, "asked") + raw, _ := puts.Encode(asked) + if _, err := putsKV.Put(b.ctx, pkey, raw); err != nil { t.Fatal(err) } for deadline := time.Now().Add(15 * time.Second); ; time.Sleep(200 * time.Millisecond) { @@ -262,8 +264,8 @@ func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) { if _, err := b.buckets[buckets.Presence].Put(b.ctx, "n1", []byte(time.Now().String())); err != nil { t.Fatal(err) } - if e, err := puts.Get(b.ctx, pkey); err == nil { - if got, err := delivery.DecodePut(e.Value()); err == nil && got.Step == delivery.PutRefused && strings.Contains(got.Words, "not assigned") { + if e, err := putsKV.Get(b.ctx, pkey); err == nil { + if got, err := puts.Decode(e.Value()); err == nil && got.Step == puts.Refused && strings.Contains(got.Words, "not assigned") { break } } @@ -290,7 +292,7 @@ func TestTheAssignerDoesNotStartWithoutABucketItReads(t *testing.T) { if _, err := placement.Put(pb.ctx, plkey, pl); err != nil { t.Fatal(err) } - if nw, err := delivery.DecodeNodeWant(pb.get(t, buckets.Wants, "want.n1.node")); err != nil || !nw.Self { + if nw, err := mwc.DecodeNodeWant(pb.get(t, buckets.Wants, "want.n1.node")); err != nil || !nw.Self { t.Fatalf("the placement state made after the start was not read: n1's own want = %+v, %v", nw, err) } } @@ -441,12 +443,12 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { } } - wkey, _ := delivery.WantKey("ace", "web") - want, err := delivery.DecodeWant(b.get(t, buckets.Wants, wkey)) - if err != nil || want.FP != fNew { + wkey, _ := mwc.WantKey("ace", "web") + want, err := mwc.DecodeWant(b.get(t, buckets.Wants, wkey)) + if err != nil || want.Fp != fNew { t.Fatalf("the canary ace's want = %+v, %v", want, err) } - if w := b.wantNow(t, "shanks", "web", "shanks' first want at what it runs", func(w delivery.Want) bool { return w.FP != "" }); w.FP != fOld || w.Composed != "" || !w.Self || w.Newest != fNew { + if w := b.wantNow(t, "shanks", "web", "shanks' first want at what it runs", func(w mwc.Want) bool { return w.Fp != "" }); w.Fp != fOld || w.Composed != "" || !w.Self || w.Newest != fNew { t.Errorf("shanks' want = %+v, want the fingerprint it runs, the node composing it, held back from F", w) } // DLV-O5 (ADR 0615 §5, ADR 0619 §2): each want is said on the assigner's views family, its value whole at its revision, @@ -474,7 +476,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { // there by itself: the condition is cleared. healthyF, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) b.putJSON(t, buckets.Have, "ace.web", healthyF) - if w := b.wantNow(t, "g14", "web", "g14's first want at F once the canary runs it healthy", func(w delivery.Want) bool { return w.FP != "" }); w.FP != fNew || w.Composed != "" || !w.Self || w.Newest != "" { + if w := b.wantNow(t, "g14", "web", "g14's first want at F once the canary runs it healthy", func(w mwc.Want) bool { return w.Fp != "" }); w.Fp != fNew || w.Composed != "" || !w.Self || w.Newest != "" { t.Errorf("g14's want = %+v, want F, the node composing it", w) } cleared := false @@ -497,14 +499,14 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { pkey, _ := mmac.LegacyKey(node) b.putJSON(t, plc.PlacementBucket, pkey, pl) } - if w := b.wantNow(t, "ace", "web", "ace's want once placed: the build alone, composed by the node", func(w delivery.Want) bool { return w.Self }); w.Composed != "" || w.FP != fNew { + if w := b.wantNow(t, "ace", "web", "ace's want once placed: the build alone, composed by the node", func(w mwc.Want) bool { return w.Self }); w.Composed != "" || w.Fp != fNew { t.Errorf("ace's want once placed = %+v", w) } - nkey, _ := delivery.WantNodeKey("ace") + nkey, _ := mwc.WantNodeKey("ace") var self bool for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && !self; time.Sleep(100 * time.Millisecond) { if e, err := b.buckets[buckets.Wants].Get(b.ctx, nkey); err == nil { - nw, err := delivery.DecodeNodeWant(e.Value()) + nw, err := mwc.DecodeNodeWant(e.Value()) self = err == nil && nw.Self } } @@ -555,15 +557,15 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { } runsOld, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: toolOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) b.putJSON(t, buckets.Have, "ace.tool", runsOld) - if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w delivery.Want) bool { return w.FP == fpOf('4') }); w.FP != fpOf('4') { + if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w mwc.Want) bool { return w.Fp == fpOf('4') }); w.Fp != fpOf('4') { t.Errorf("tool's want = %+v", w) } - b.wantNow(t, "ace", "db", "the provider's want at its fingerprint", func(w delivery.Want) bool { return w.FP == fpOf('2') }) + b.wantNow(t, "ace", "db", "the provider's want at its fingerprint", func(w mwc.Want) bool { return w.Fp == fpOf('2') }) if e, err := b.buckets[buckets.Wants].Get(b.ctx, "want.ace.app"); err == nil { t.Errorf("app was wanted while its provision was asked: %s", e.Value()) } asked(provisions.Provided, "") - if w := b.wantNow(t, "ace", "app", "the consumer's want once its provision is provided", func(w delivery.Want) bool { return w.FP == fpOf('3') }); w.FP != fpOf('3') { + if w := b.wantNow(t, "ace", "app", "the consumer's want once its provision is provided", func(w mwc.Want) bool { return w.Fp == fpOf('3') }); w.Fp != fpOf('3') { t.Errorf("app's want = %+v", w) } @@ -618,7 +620,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { b.putJSON(t, buckets.Have, "ace.forms", runsFormsOld) b.place(t, "ace", "forms") formsAt(formsOld) - b.wantNow(t, "ace", "forms", "the want at what ace runs, a build in the first form", func(w delivery.Want) bool { return w.FP == formsOld }) + b.wantNow(t, "ace", "forms", "the want at what ace runs, a build in the first form", func(w mwc.Want) bool { return w.Fp == formsOld }) formsAt(formsNew) statement := func(node string, ok func(enginereads.Statement) bool) enginereads.Statement { var last enginereads.Statement @@ -636,16 +638,16 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { if s := statement("g14", func(s enginereads.Statement) bool { return !s.Answered }); s.From != enginereads.FromNone { t.Errorf("g14, whose engine answers nothing and which has no record, reads %+v", s) } - if w := b.wantNow(t, "ace", "forms", "the want held on ace, naming the form", func(w delivery.Want) bool { + if w := b.wantNow(t, "ace", "forms", "the want held on ace, naming the form", func(w mwc.Want) bool { return w.Newest == formsNew && strings.Contains(w.Words, "form:new") - }); w.FP != formsOld || w.Rule != delivery.RuleComposed { + }); w.Fp != formsOld || w.Rule != mwc.RuleComposed { t.Errorf("forms' want = %+v, want it held at what ace runs under rule 7", w) } statedMu.Lock() stated = append(slices.Clone(stated), "form:new") statedMu.Unlock() say(enginereads.ReadsStarted) - b.wantNow(t, "ace", "forms", "the want moved once ace's engine said it reads the form", func(w delivery.Want) bool { return w.FP == formsNew }) + b.wantNow(t, "ace", "forms", "the want moved once ace's engine said it reads the form", func(w mwc.Want) bool { return w.Fp == formsNew }) // assigner_engine_reads asks ace's engine again through the state: the assigner answers there with what it states now. again, err := enginereads.AskAgainOf(bedState{b, engineReadsBucket}, "ace", "n1.ada", "a person asks", time.Now(), 15*time.Second) if err != nil || again.From != enginereads.FromVerb || !slices.Contains(again.ManifestReads, "form:new") || again.AskAgain != nil { @@ -695,9 +697,9 @@ func TestAHeldFingerprintIsAskedAbout(t *testing.T) { b.putJSON(t, buckets.Have, "ace.web", failed) b.place(t, "ace", "web") b.start(t) - hkey, _ := delivery.HeldKey("web", fNew) + hkey, _ := mwc.HeldKey("web", fNew) // 1906/H3: the held record names the machine and keeps the health, when and the node's words. - if h, err := delivery.DecodeHeld(b.get(t, buckets.Wants, hkey)); err != nil || h.Node != "ace" || + if h, err := mwc.DecodeHeld(b.get(t, buckets.Wants, hkey)); err != nil || h.Node != "ace" || !strings.HasPrefix(h.Words, "health failed, phase running, seen at ") || !strings.HasSuffix(h.Words, ": connection refused") { t.Fatalf("held = %+v, %v", h, err) } @@ -751,8 +753,8 @@ func TestAFailedDataModuleIsAskedToRestore(t *testing.T) { b.putJSON(t, buckets.Presence, "ace", on) failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "connection refused"}) b.putJSON(t, buckets.Have, "ace.mail", failed) - wantKey, _ := delivery.WantKey("ace", "mail") - want, _ := delivery.EncodeWant(delivery.Want{FP: fNew, Composed: "sha256:" + strings.Repeat("c", 64)}) + wantKey, _ := mwc.WantKey("ace", "mail") + want, _ := mwc.EncodeWant(mwc.Want{Fp: fNew, Composed: "sha256:" + strings.Repeat("c", 64)}) b.putJSON(t, buckets.Wants, wantKey, want) b.place(t, "ace", "mail") b.start(t) @@ -762,7 +764,7 @@ func TestAFailedDataModuleIsAskedToRestore(t *testing.T) { if err != nil || len(a.Ask.Options) != 2 || a.Ask.Options[0].ID != "restore" || a.Ask.Options[1].ID != "leave-stopped" { t.Fatalf("the restore ask = %+v, %v", a, err) } - if w, err := delivery.DecodeWant(b.get(t, buckets.Wants, wantKey)); err != nil || w.FP != fNew { + if w, err := mwc.DecodeWant(b.get(t, buckets.Wants, wantKey)); err != nil || w.Fp != fNew { t.Errorf("the data module was set back: want = %+v, %v", w, err) } key := "" @@ -825,7 +827,7 @@ func TestTheAssignerReplacedResumesWithNothingLostOrRepeated(t *testing.T) { } time.Sleep(time.Second) // and no pass of the second writes any again for _, name := range names { - key, _ := delivery.WantKey("ace", name) + key, _ := mwc.WantKey("ace", name) history, err := b.buckets[buckets.Wants].History(b.ctx, key) if err != nil { t.Errorf("the want of %s was never written: %v", name, err) @@ -854,10 +856,10 @@ func (b *bed) fixture(t *testing.T, name, fp, manifest string) { b.place(t, "ace", name) } -func (b *bed) putWant(t *testing.T, node, module string, w delivery.Want) { +func (b *bed) putWant(t *testing.T, node, module string, w mwc.Want) { t.Helper() - key, _ := delivery.WantKey(node, module) - v, err := delivery.EncodeWant(w) + key, _ := mwc.WantKey(node, module) + v, err := mwc.EncodeWant(w) if err != nil { t.Fatal(err) } @@ -865,13 +867,13 @@ func (b *bed) putWant(t *testing.T, node, module string, w delivery.Want) { } // wantNow waits until the want of module on node satisfies ok and answers it. -func (b *bed) wantNow(t *testing.T, node, module, what string, ok func(delivery.Want) bool) delivery.Want { +func (b *bed) wantNow(t *testing.T, node, module, what string, ok func(mwc.Want) bool) mwc.Want { t.Helper() - key, _ := delivery.WantKey(node, module) - var last delivery.Want + key, _ := mwc.WantKey(node, module) + var last mwc.Want for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { if e, err := b.buckets[buckets.Wants].Get(b.ctx, key); err == nil { - if w, err := delivery.DecodeWant(e.Value()); err == nil { + if w, err := mwc.DecodeWant(e.Value()); err == nil { last = w if ok(w) { return w @@ -920,16 +922,16 @@ func TestAReleaseAnswerWritesTheWantAgain(t *testing.T) { b.putJSON(t, buckets.Have, "ace.web", healthy) // it ran the older fingerprint healthy before failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "connection refused"}) b.putJSON(t, buckets.Have, "ace.web", failed) - b.putWant(t, "ace", "web", delivery.Want{FP: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) + b.putWant(t, "ace", "web", mwc.Want{Fp: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) b.start(t) - w := b.wantNow(t, "ace", "web", "set back to the older fingerprint", func(w delivery.Want) bool { return w.FP == fOld }) + w := b.wantNow(t, "ace", "web", "set back to the older fingerprint", func(w mwc.Want) bool { return w.Fp == fOld }) if w.Newest != fNew { t.Fatalf("the set-back want = %+v", w) } hold, _ := asks.HoldAskKey("web", fNew) b.get(t, buckets.Asks, hold) b.answer(t, hold, "release") - w = b.wantNow(t, "ace", "web", "the held fingerprint's want written again", func(w delivery.Want) bool { return w.FP == fNew }) + w = b.wantNow(t, "ace", "web", "the held fingerprint's want written again", func(w mwc.Want) bool { return w.Fp == fNew }) if w.Composed != "" || !w.Self || w.Rule != 0 { t.Errorf("the want = %+v, want F by the rules, the node composing it", w) } @@ -967,7 +969,7 @@ func TestABackupAnswerIsWrittenIntoTheNodesWant(t *testing.T) { fNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) b.fixture(t, "web", fNew, `{"module":"web","version":"1"}`) b.compose(t, "ace", 'b') - b.putWant(t, "ace", "web", delivery.Want{FP: fNew, Self: true}) // a placed node's want names the build alone + b.putWant(t, "ace", "web", mwc.Want{Fp: fNew, Self: true}) // a placed node's want names the build alone held, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseHeld, Health: nodeengineclient.HealthWaits, Detail: "no space left"}) b.putJSON(t, buckets.Have, "ace.web", held) b.start(t) @@ -977,14 +979,14 @@ func TestABackupAnswerIsWrittenIntoTheNodesWant(t *testing.T) { t.Fatalf("the backup ask = %+v", a) } // an answer to another ask is no answer to this one - b.answerTo(t, akey, delivery.BackupSkip, "someone-else") + b.answerTo(t, akey, mwc.BackupSkip, "someone-else") time.Sleep(time.Second) - if w := b.wantNow(t, "ace", "web", "the want", func(delivery.Want) bool { return true }); w.Backup != "" { + if w := b.wantNow(t, "ace", "web", "the want", func(mwc.Want) bool { return true }); w.Backup != "" { t.Fatalf("an answer to another ask was taken: %+v", w) } - b.answerTo(t, akey, delivery.BackupAgain, a.Ask.ID) - w1 := b.wantNow(t, "ace", "web", "the answer in the want", func(w delivery.Want) bool { return w.Backup == delivery.BackupAgain }) - if w1.FP != fNew || w1.Composed != "" || !w1.Self { + b.answerTo(t, akey, mwc.BackupAgain, a.Ask.ID) + w1 := b.wantNow(t, "ace", "web", "the answer in the want", func(w mwc.Want) bool { return w.Backup == mwc.BackupAgain }) + if w1.Fp != fNew || w1.Composed != "" || !w1.Self { t.Errorf("the want = %+v", w1) } // the node retries and the backup fails again: a new have value, asked again under the next number @@ -994,8 +996,8 @@ func TestABackupAnswerIsWrittenIntoTheNodesWant(t *testing.T) { if a2.Ask.ID == a.Ask.ID { t.Fatalf("asked again under the same id") } - b.answerTo(t, akey, delivery.BackupAgain, a2.Ask.ID) - w2 := b.wantNow(t, "ace", "web", "the second answer another want value", func(w delivery.Want) bool { return w.Backup == delivery.BackupAgain && w.Words != w1.Words }) + b.answerTo(t, akey, mwc.BackupAgain, a2.Ask.ID) + w2 := b.wantNow(t, "ace", "web", "the second answer another want value", func(w mwc.Want) bool { return w.Backup == mwc.BackupAgain && w.Words != w1.Words }) if w2.Words == w1.Words { t.Errorf("the second answer left the want as it was: %+v", w2) } @@ -1013,9 +1015,9 @@ func TestAHoldIsLiftedOnceEveryNodeRunsItHealthy(t *testing.T) { b.compose(t, "ace", 'b') failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "connection refused"}) b.putJSON(t, buckets.Have, "ace.web", failed) - b.putWant(t, "ace", "web", delivery.Want{FP: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) + b.putWant(t, "ace", "web", mwc.Want{Fp: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) b.start(t) - hkey, _ := delivery.HeldKey("web", fNew) + hkey, _ := mwc.HeldKey("web", fNew) akey, _ := asks.HoldAskKey("web", fNew) b.get(t, buckets.Wants, hkey) b.get(t, buckets.Asks, akey) @@ -1027,7 +1029,7 @@ func TestAHoldIsLiftedOnceEveryNodeRunsItHealthy(t *testing.T) { } for k, c := range b.conditionsSaid(t) { if strings.HasPrefix(k, "module.web.") && c.State == occ.StateCleared && strings.HasSuffix(k, ".held") { - if w := b.wantNow(t, "ace", "web", "the want at the lifted fingerprint", func(w delivery.Want) bool { return w.FP == fNew }); w.Rule != 0 { + if w := b.wantNow(t, "ace", "web", "the want at the lifted fingerprint", func(w mwc.Want) bool { return w.Fp == fNew }); w.Rule != 0 { t.Errorf("the want = %+v", w) } return @@ -1047,9 +1049,9 @@ func TestAHoldWrittenAgainAfterALiftAndARestartIsAskedAgain(t *testing.T) { failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "connection refused"}) healthy, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) b.putJSON(t, buckets.Have, "ace.web", failed) - b.putWant(t, "ace", "web", delivery.Want{FP: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) + b.putWant(t, "ace", "web", mwc.Want{Fp: fNew, Composed: "sha256:" + strings.Repeat("b", 64)}) _, stop := b.startStoppable(t) - hkey, _ := delivery.HeldKey("web", fNew) + hkey, _ := mwc.HeldKey("web", fNew) akey, _ := asks.HoldAskKey("web", fNew) first, err := decodeAsk(akey, b.get(t, buckets.Asks, akey)) if err != nil { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 8711d6f5..90437261 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "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/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" @@ -133,15 +134,15 @@ func (p *passer) nodeWants(ctx context.Context) { placed := p.w.placedNodes() // A node the catalogue places composes its own composition (ADR 0547 §2): its own want names none. for node := range placed { - key, err := delivery.WantNodeKey(node) + key, err := mwc.WantNodeKey(node) if err != nil { continue } - if before, err := delivery.DecodeNodeWant(written[key]); err == nil && before.Self { + if before, err := mwc.DecodeNodeWant(written[key]); err == nil && before.Self { continue } - want := delivery.NodeWant{Self: true, Words: "the node composes its own composition (ADR 0547 §2)"} - p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return delivery.EncodeNodeWant(want) }) + want := mwc.NodeWant{Self: true, Words: "the node composes its own composition (ADR 0547 §2)"} + p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return mwc.EncodeNodeWant(want) }) } } @@ -224,7 +225,7 @@ func (p *passer) wants(ctx context.Context) { }, Release: func(fp string) (string, bool) { return p.w.release(name, fp) }} if fp != "" { - held, _ := delivery.HeldKey(name, fp) + held, _ := mwc.HeldKey(name, fp) grant, _ := grantsstate.PartGrantKey(name, fp) hold, _ := asks.HoldAskKey(name, fp) m.Released = p.answeredWith(hold, "release") @@ -266,10 +267,10 @@ func (p *passer) wants(ctx context.Context) { } } if out.Held != nil { - key, err := delivery.HeldKey(name, fp) + key, err := mwc.HeldKey(name, fp) if err == nil { held := *out.Held - p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return delivery.EncodeHeld(held) }) + p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return mwc.EncodeHeld(held) }) } } if out.Lifted { @@ -286,13 +287,13 @@ func (p *passer) wants(ctx context.Context) { p.recordAsk(ctx, name, fp, need) } for node, want := range out.Wants { - key, err := delivery.WantKey(node, name) + key, err := mwc.WantKey(node, name) if err != nil { p.logf("no want of %s on %s is written: %v", name, node, err) continue } want := want - p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return delivery.EncodeWant(want) }) + p.put(ctx, buckets.Wants, key, func() ([]byte, error) { return mwc.EncodeWant(want) }) } } } @@ -300,7 +301,7 @@ func (p *passer) wants(ctx context.Context) { // sayNoWant says in the journal why a node of module gets no want after this pass, from the rule the outcome names for it // (issue 1945: a new assignment whose trunk build was not done was held back with nothing said), once per reason, and says // when the node has a want again. A node with a live want, written before and kept or written in this pass, has one. -func (p *passer) sayNoWant(module string, nodes []string, before map[string]delivery.Want, out assign.Outcome) { +func (p *passer) sayNoWant(module string, nodes []string, before map[string]mwc.Want, out assign.Outcome) { if p.noWant == nil { p.noWant = map[string]string{} } @@ -424,7 +425,7 @@ func gatherNeither(into map[string]neitherOne, module string, out assign.Outcome // want for it (written before and kept, or written in this pass), else why it has none. sayNeither clears a neither condition in // these words, so a clear never says a want the node does not hold (issue 1945: "nats on novox has a want again" while the bus bar // left it with none). -func leftNeither(into map[string]string, m assign.Module, before map[string]delivery.Want, out assign.Outcome) { +func leftNeither(into map[string]string, m assign.Module, before map[string]mwc.Want, out assign.Outcome) { for _, node := range m.Nodes { if _, on := out.Neither[node]; on { continue @@ -549,7 +550,7 @@ func (p *passer) put(ctx context.Context, bucket, key string, encode func() ([]b // holdAsk writes, once, the ask about a held fingerprint (ADR 0528 clause 6, ADR 0437 clause 10): the first ask that needs // no proof of who answered, which the view may answer and the teller fills. In release 5 an answer changes nothing on a // node (wants are only compared, ADR 0429 §7), and the options say so. -func (p *passer) holdAsk(ctx context.Context, module, fp string, fresh *delivery.Held) { +func (p *passer) holdAsk(ctx context.Context, module, fp string, fresh *mwc.Held) { key, err := asks.HoldAskKey(module, fp) if err != nil || p.w.has(buckets.Asks, key) { return @@ -567,7 +568,7 @@ func (p *passer) holdAsk(ctx context.Context, module, fp string, fresh *delivery // (issue 1906): the held record and its ask (the teller then takes the ask back from the operator), and the held condition is // cleared saying why, once. A hold of the same fingerprint written later is said and asked again. func (p *passer) lift(ctx context.Context, module, fp string) { - held, err := delivery.HeldKey(module, fp) + held, err := mwc.HeldKey(module, fp) if err != nil { return } @@ -651,9 +652,9 @@ func (p *passer) fingerprintsOf(node, module, trunkFP string) []string { } } add(trunkFP) - if key, err := delivery.WantKey(node, module); err == nil { - if w, err := delivery.DecodeWant(p.w.value(buckets.Wants, key)); err == nil { - add(w.FP) + if key, err := mwc.WantKey(node, module); err == nil { + if w, err := mwc.DecodeWant(p.w.value(buckets.Wants, key)); err == nil { + add(w.Fp) } } if key, err := nodeengineclient.HaveKey(node, module); err == nil { @@ -706,21 +707,21 @@ func (p *passer) remove(ctx context.Context, bucket, key string) { } // heldHold is the hold of fp as written: the node whose health check failed and its words. -func (p *passer) heldHold(module, fp string, fresh *delivery.Held) delivery.Held { +func (p *passer) heldHold(module, fp string, fresh *mwc.Held) mwc.Held { if fresh != nil { return *fresh } - key, err := delivery.HeldKey(module, fp) + key, err := mwc.HeldKey(module, fp) if err != nil { - return delivery.Held{} + return mwc.Held{} } - h, _ := delivery.DecodeHeld(p.w.read(buckets.Wants)[key]) + h, _ := mwc.DecodeHeld(p.w.read(buckets.Wants)[key]) return h } // heldCondition raises, once, the condition for a fingerprint the assigner holds (ADR 0429 §6, ADR 0539 §19): the module, the // node whose health check failed and the check's words. A module holding data needs the operator's answer to its restore ask. -func (p *passer) heldCondition(ctx context.Context, module, fp string, fresh *delivery.Held, data bool) { +func (p *passer) heldCondition(ctx context.Context, module, fp string, fresh *mwc.Held, data bool) { h := p.heldHold(module, fp, fresh) c := assign.HeldCondition(module, fp, h.Node, h.Words, true, data) if p.say == nil || p.said[c.Key+".raised"] { @@ -812,7 +813,7 @@ func (p *passer) backups(ctx context.Context) { if !ok || want.Removed { continue } - akey, err := asks.BackupAskKey(node, module, want.FP) + akey, err := asks.BackupAskKey(node, module, want.Fp) if err != nil { continue } @@ -827,21 +828,21 @@ func (p *passer) backups(ctx context.Context) { } answer, answered := p.w.answer(akey) if router, _, _ := strings.Cut(answer.Warrant, "@"); !answered || router != currentAsk.ID || - (answer.Option != delivery.BackupAgain && answer.Option != delivery.BackupSkip) { + (answer.Option != mwc.BackupAgain && answer.Option != mwc.BackupSkip) { continue // not answered yet: the ask stands } words := assign.BackupAnswerWords(answer.Option, current.Asking) haveChanges := p.w.changed(buckets.Have, key) if want.Backup != answer.Option || want.Words != words { want.Backup, want.Words = answer.Option, words - if wkey, err := delivery.WantKey(node, module); err == nil { + if wkey, err := mwc.WantKey(node, module); err == nil { w := want - p.put(ctx, buckets.Wants, wkey, func() ([]byte, error) { return delivery.EncodeWant(w) }) + p.put(ctx, buckets.Wants, wkey, func() ([]byte, error) { return mwc.EncodeWant(w) }) p.answeredAt[currentAsk.ID] = haveChanges } continue } - if answer.Option != delivery.BackupAgain { + if answer.Option != mwc.BackupAgain { continue } // The answer is in the want. The node retried and the backup failed again when the have took another value since. @@ -854,10 +855,10 @@ func (p *passer) backups(ctx context.Context) { } // backupAsk writes the backup ask at its asking and raises the condition of the failed backup, once. -func (p *passer) backupAsk(ctx context.Context, key, node, module string, want delivery.Want, words string, asking int) { - a := assign.BackupAsk(key, node, module, want.FP, want.Composed, words, asking, time.Now()) +func (p *passer) backupAsk(ctx context.Context, key, node, module string, want mwc.Want, words string, asking int) { + a := assign.BackupAsk(key, node, module, want.Fp, want.Composed, words, asking, time.Now()) p.put(ctx, buckets.Asks, key, func() ([]byte, error) { return assign.EncodeAsk(key, a) }) - c := assign.BackupCondition(node, module, want.FP, words, true) + c := assign.BackupCondition(node, module, want.Fp, words, true) if p.say == nil || p.said[c.Key] { return } @@ -874,7 +875,7 @@ func (p *passer) backupAsk(ctx context.Context, key, node, module string, want d // the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again. A // move of the build alone (the have says so) to a build an approved release carries is given its yes by that Approve, the // want naming the release, with no ask (ADR 0601 §1). -func (p *passer) yeses(ctx context.Context, module string, m assign.Module, before map[string]delivery.Want, out *assign.Outcome) { +func (p *passer) yeses(ctx context.Context, module string, m assign.Module, before map[string]mwc.Want, out *assign.Outcome) { for _, node := range m.Nodes { if m.Self == nil || !m.Self(node) { continue @@ -891,7 +892,7 @@ func (p *passer) yeses(ctx context.Context, module string, m assign.Module, befo continue } yes, release := "", "" - if had && was.FP == w.FP { + if had && was.Fp == w.Fp { yes, release = was.Yes, was.Release } key, err := nodeengineclient.HaveKey(node, module) @@ -902,16 +903,16 @@ func (p *passer) yeses(ctx context.Context, module string, m assign.Module, befo switch { case m.Allowed != nil && m.Allowed(node, h.Waits): yes, release = h.Waits, "" - case h.BuildAlone && h.Fp != w.FP && m.Release != nil: + case h.BuildAlone && h.Fp != w.Fp && m.Release != nil: // A move of the build alone, to the build an approved release carries: that Approve is the yes (ADR 0601 §1), // named in the want. A move that changes a setting or a secret too, or one the node could not tell, is asked. - if r, ok := m.Release(w.FP); ok { + if r, ok := m.Release(w.Fp); ok { yes, release = h.Waits, r break } - p.recordAsk(ctx, module, w.FP, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) + p.recordAsk(ctx, module, w.Fp, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) default: - p.recordAsk(ctx, module, w.FP, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) + p.recordAsk(ctx, module, w.Fp, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) } } // A want carried over from before the node composed its own may still name the controller's composition; a placed diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 24ad35a0..0140f36b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -8,12 +8,14 @@ import ( "fmt" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + 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/seats/meshcatalog" "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" "mesh-delivery-assigner/internal/buckets" + "mesh-delivery-assigner/internal/puts" "slices" "strings" "testing" @@ -103,7 +105,7 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) { asksKV, wantsKV := &memKV{values: map[string][]byte{}}, &memKV{values: map[string][]byte{}} kvs := kvSet{buckets.Asks: asksKV, buckets.Wants: wantsKV} key, _ := asks.HoldAskKey("web", fpTest) - held := &delivery.Held{Node: "ace", Words: "health failed"} + held := &mwc.Held{Node: "ace", Words: "health failed"} first := newPasser(kvs, time.Date(2026, 10, 25, 1, 0, 0, 0, time.UTC)) first.holdAsk(context.Background(), "web", fpTest, held) a1, err := decodeAsk(key, asksKV.values[key]) @@ -324,8 +326,8 @@ func putOfTheBus(t *testing.T) { fp := meshcatalogclient.SourcePrefix2 + "fd497ab8" + strings.Repeat("0", 56) digest := "sha256:" + strings.Repeat("d", 64) mem := func() *memKV { return &memKV{values: map[string][]byte{}} } - puts, asksKV, wantsKV := mem(), mem(), mem() - kvs := kvSet{buckets.Puts: puts, buckets.Asks: asksKV, buckets.Wants: wantsKV} + putsKV, asksKV, wantsKV := mem(), mem(), mem() + kvs := kvSet{buckets.Puts: putsKV, buckets.Asks: asksKV, buckets.Wants: wantsKV} p := newPasser(kvs, now) bus := &fakeBus{calls: make(chan string, 8)} woke := make(chan struct{}, 8) // each act's goroutine wakes a pass once it is done @@ -360,7 +362,7 @@ func putOfTheBus(t *testing.T) { buildKV.values[bkey] = built modules := mem() modules.values["nats"] = mod - s := newPutTools(memStore{putsState: puts, builds.BuilderModule + ".builds": buildKV, delivery.IndexerModule + ".modules": modules}) + s := newPutTools(memStore{putsState: putsKV, builds.BuilderModule + ".builds": buildKV, delivery.IndexerModule + ".modules": modules}) s.now = func() time.Time { return now } was := answerWithin answerWithin = 0 @@ -378,21 +380,21 @@ func putOfTheBus(t *testing.T) { if err != nil { t.Fatal(err) } - if a := ans.(putAnswer); a.Step != delivery.PutAsked || !a.Hold || !strings.Contains(a.Follow, "mesh_wait") { + if a := ans.(putAnswer); a.Step != puts.Asked || !a.Hold || !strings.Contains(a.Follow, "mesh_wait") { t.Fatalf("put answered %+v", a) } - pkey, _ := delivery.PutKey("novox", "nats") - step := func() delivery.Put { + pkey, _ := puts.Key("novox", "nats") + step := func() puts.Put { t.Helper() - world(buckets.Puts, pkey, puts.values[pkey]) + world(buckets.Puts, pkey, putsKV.values[pkey]) p.pass(ctx) - got, err := delivery.DecodePut(puts.values[pkey]) + got, err := puts.Decode(putsKV.values[pkey]) if err != nil { t.Fatal(err) } return got } - if got := step(); got.Step != delivery.PutCompose || !strings.Contains(got.Words, "no composition") { + if got := step(); got.Step != puts.Compose || !strings.Contains(got.Words, "no composition") { t.Fatalf("the first pass left the put at %s: %s", got.Step, got.Words) } select { @@ -406,7 +408,7 @@ func putOfTheBus(t *testing.T) { world(buckets.Grants, gkey, g) got := step() akey, _ := asks.RecordAskKey("novox", "nats", digest) - if got.Step != delivery.PutPerson || got.Ask != akey { + if got.Step != puts.Person || got.Ask != akey { t.Fatalf("composed, the put stands at %s (%s), ask %q", got.Step, got.Words, got.Ask) } a, err := decodeAsk(akey, asksKV.values[akey]) @@ -419,13 +421,13 @@ func putOfTheBus(t *testing.T) { world(buckets.Asks, akey, asksKV.values[akey]) yes, _ := delivery.EncodeAnswer(delivery.Answer{Option: assign.OptionYes, Warrant: a.Ask.ID + "@2026-10-25T22:01:00Z", Words: "the operator, verified"}) world(buckets.Answers, akey, yes) - if got := step(); got.Step != delivery.PutSnapshot { + if got := step(); got.Step != puts.Snapshot { t.Fatalf("on the warrant the put stands at %s: %s", got.Step, got.Words) } <-woke got = step() - w, err := delivery.DecodeWant(wantsKV.values["want.novox.nats"]) - if got.Step != delivery.PutWanted || err != nil || w.FP != fp || w.Composed != digest || !w.Put || !w.Hold { + w, err := mwc.DecodeWant(wantsKV.values["want.novox.nats"]) + if got.Step != puts.Wanted || err != nil || w.Fp != fp || w.Composed != digest || !w.Put || !w.Hold { t.Fatalf("snapshotted, the put stands at %s (%s); the want %+v, %v", got.Step, got.Words, w, err) } if call := <-bus.calls; !strings.HasPrefix(call, "seat:mesh-hand-acts.warranted ") || !strings.Contains(call, a.Ask.ID) { // ADR 0565 §11: into the seat @@ -434,17 +436,17 @@ func putOfTheBus(t *testing.T) { world(buckets.Wants, "want.novox.nats", wantsKV.values["want.novox.nats"]) running, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: digest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) world(buckets.Have, hkey, running) - if got := step(); got.Step != delivery.PutHolding { + if got := step(); got.Step != puts.Holding { t.Fatalf("running healthy, the put stands at %s: %s", got.Step, got.Words) } if _, err := s.doRelease(map[string]any{"node": "novox", "module": "nats", "why": "done"}, "shanks.node-tools"); err != nil { t.Fatal(err) } - if got := step(); got.Step != delivery.PutReleased { + if got := step(); got.Step != puts.Released { t.Fatalf("released, the put stands at %s", got.Step) } listed, _ := s.doPuts(map[string]any{"module": "nats"}) - if l := listed.(map[string]any)["puts"].([]putAnswer); len(l) != 1 || l[0].Step != delivery.PutReleased || len(l[0].Steps) < 6 { + if l := listed.(map[string]any)["puts"].([]putAnswer); len(l) != 1 || l[0].Step != puts.Released || len(l[0].Steps) < 6 { t.Errorf("puts lists %+v", l) } if len(bus.events) < 5 || !strings.Contains(strings.Join(bus.events, "\n"), "holding") { @@ -459,18 +461,18 @@ func putOfTheBus(t *testing.T) { } again := step() a2, err := decodeAsk(akey, asksKV.values[akey]) - if again.Step != delivery.PutPerson || err != nil || a2.Ask.ID == a.Ask.ID { + if again.Step != puts.Person || err != nil || a2.Ask.ID == a.Ask.ID { t.Fatalf("put again, it stands at %s (%s); its ask %q, %v; the first ask was %q", again.Step, again.Words, a2.Ask.ID, err, a.Ask.ID) } world(buckets.Asks, akey, asksKV.values[akey]) yes2, _ := delivery.EncodeAnswer(delivery.Answer{Option: assign.OptionYes, Warrant: a2.Ask.ID + "@2026-10-25T22:03:00Z", Words: "the operator, verified"}) world(buckets.Answers, akey, yes2) - if got := step(); got.Step != delivery.PutSnapshot { + if got := step(); got.Step != puts.Snapshot { t.Fatalf("on the second warrant the put stands at %s: %s", got.Step, got.Words) } <-woke // Released between the pass's read and its write: the pass writes no want for the put that no longer stands. - oldWant, _ := delivery.EncodeWant(delivery.Want{FP: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)}) + oldWant, _ := mwc.EncodeWant(mwc.Want{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)}) wantsKV.values["want.novox.nats"] = oldWant world(buckets.Wants, "want.novox.nats", oldWant) if _, err := s.doRelease(map[string]any{"node": "novox", "module": "nats", "why": "changed my mind"}, "shanks.node-tools"); err != nil { @@ -505,7 +507,7 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { var logged []string p.logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } one := map[string]neitherOne{"g14.screen-lock": {node: "g14", module: "screen-lock", - holding: assign.Holding{Rule: delivery.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}}} + holding: assign.Holding{Rule: mwc.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}}} p.sayNeither(one, nil, nil) // the say fails p.sayNeither(one, nil, nil) p.sayNeither(one, nil, nil) @@ -526,7 +528,7 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { } // 1945: a module that leaves neither list with no want is cleared in words saying so, never "has a want again". The bus is // on neither list by a rule as any module (ADR 0601 §1), nothing for the operator to do. - bus := map[string]neitherOne{"novox.nats": {node: "novox", module: "nats", holding: assign.Holding{Rule: delivery.RuleGrant, Words: "no credential"}}} + bus := map[string]neitherOne{"novox.nats": {node: "novox", module: "nats", holding: assign.Holding{Rule: mwc.RuleGrant, Words: "no credential"}}} p.sayNeither(bus, nil, nil) if c := said[len(said)-1]; c.State != occ.StateRaised || !strings.HasPrefix(c.Explanation, occ.NothingToDo) || !strings.Contains(c.Explanation, "rule 6 holds its first want back: no credential") { t.Errorf("the bus's neither condition = %+v", c) @@ -543,23 +545,23 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { // 1945/AC21: the pass gathers the modules on neither list of every module's outcome, keyed by node and module. func testThePassGathersTheModulesOnNeitherList(t *testing.T) { got := map[string]neitherOne{} - gatherNeither(got, "screen-lock", assign.Outcome{Neither: map[string]assign.Holding{"g14": {Rule: delivery.RuleGrant, Words: "w"}}}) + gatherNeither(got, "screen-lock", assign.Outcome{Neither: map[string]assign.Holding{"g14": {Rule: mwc.RuleGrant, Words: "w"}}}) gatherNeither(got, "rofi", assign.Outcome{}) - if len(got) != 1 || got["g14.screen-lock"].node != "g14" || got["g14.screen-lock"].module != "screen-lock" || got["g14.screen-lock"].holding.Rule != delivery.RuleGrant { + if len(got) != 1 || got["g14.screen-lock"].node != "g14" || got["g14.screen-lock"].module != "screen-lock" || got["g14.screen-lock"].holding.Rule != mwc.RuleGrant { t.Errorf("gathered = %+v", got) } // 1945 (the bus bar): a node off neither list is cleared in words that say why it has no want; an operator's put running // through its steps owns the want, so it is named, never "the node does not run it". for _, c := range []struct { name string - puts map[string]delivery.Put - before map[string]delivery.Want + puts map[string]puts.Put + before map[string]mwc.Want why string }{ - {"a live want", nil, map[string]delivery.Want{"novox": {FP: fpTest}}, ""}, - {"a put on its steps", map[string]delivery.Put{"novox": {FP: fpTest, Step: delivery.PutPerson}}, nil, "its put waits on its own steps and the operator's answer"}, + {"a live want", nil, map[string]mwc.Want{"novox": {Fp: fpTest}}, ""}, + {"a put on its steps", map[string]puts.Put{"novox": {FP: fpTest, Step: puts.Person}}, nil, "its put waits on its own steps and the operator's answer"}, {"nothing", nil, nil, "the node does not run it"}, - {"an ended put", map[string]delivery.Put{"novox": {FP: fpTest, Step: delivery.PutRefused}}, nil, "the node does not run it"}, + {"an ended put", map[string]puts.Put{"novox": {FP: fpTest, Step: puts.Refused}}, nil, "the node does not run it"}, } { left := map[string]string{} leftNeither(left, assign.Module{Name: "nats", FP: fpTest, Nodes: []string{"novox"}, Puts: c.puts}, c.before, assign.Outcome{}) @@ -572,19 +574,19 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { p := newPasser(kvSet{}, time.Date(2026, 10, 25, 21, 30, 0, 0, time.UTC)) var logged []string p.logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } - notDone := assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Rule: delivery.RuleBuildDone, Words: "the build of rofi at 4e7b118e is not done"}}} - wanted := assign.Outcome{Wants: map[string]delivery.Want{"shanks": {FP: fpTest, Composed: "sha256:" + strings.Repeat("c", 64)}}} + notDone := assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Rule: mwc.RuleBuildDone, Words: "the build of rofi at 4e7b118e is not done"}}} + wanted := assign.Outcome{Wants: map[string]mwc.Want{"shanks": {Fp: fpTest, Composed: "sha256:" + strings.Repeat("c", 64)}}} for i, c := range []struct { - before map[string]delivery.Want + before map[string]mwc.Want out assign.Outcome line string // the line said in this pass, "" for none }{ {nil, notDone, "rofi on shanks gets no want: rule 1: the build of rofi at 4e7b118e is not done"}, {nil, notDone, ""}, - {map[string]delivery.Want{"shanks": {Removed: true}}, notDone, ""}, - {nil, assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Rule: delivery.RuleGrant, Words: "no credential"}}}, "rofi on shanks gets no want: rule 6: no credential"}, + {map[string]mwc.Want{"shanks": {Removed: true}}, notDone, ""}, + {nil, assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Rule: mwc.RuleGrant, Words: "no credential"}}}, "rofi on shanks gets no want: rule 6: no credential"}, {nil, wanted, "rofi on shanks has a want"}, - {map[string]delivery.Want{"shanks": wanted.Wants["shanks"]}, assign.Outcome{}, ""}, + {map[string]mwc.Want{"shanks": wanted.Wants["shanks"]}, assign.Outcome{}, ""}, } { logged = nil p.sayNoWant("rofi", []string{"shanks"}, c.before, c.out) @@ -668,8 +670,8 @@ func TestTheFirstPassAfterAStartClearsWhatAPreviousProcessRaised(t *testing.T) { p.w.put(buckets.Presence, "g14", on) have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpTest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) p.w.put(buckets.Have, "g14.web", have) - key, _ := delivery.WantKey("g14", "web") - want, _ := delivery.EncodeWant(delivery.Want{FP: fpTest, Composed: "sha256:" + strings.Repeat("c", 64)}) + key, _ := mwc.WantKey("g14", "web") + want, _ := mwc.EncodeWant(mwc.Want{Fp: fpTest, Composed: "sha256:" + strings.Repeat("c", 64)}) p.w.put(buckets.Wants, key, want) p.wants(context.Background()) cleared := map[string]bool{} @@ -706,8 +708,8 @@ func testAHeldModuleRaisesNoNeitherAndClearsAStandingOne(t *testing.T) { p.w.put(buckets.Presence, "shanks", on) have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpTest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed}) p.w.put(buckets.Have, "shanks.systemd-resolved", have) - hkey, _ := delivery.HeldKey("systemd-resolved", fpTest) - held, _ := delivery.EncodeHeld(delivery.Held{Node: "shanks", Words: "health failed"}) + hkey, _ := mwc.HeldKey("systemd-resolved", fpTest) + held, _ := mwc.EncodeHeld(mwc.Held{Node: "shanks", Words: "health failed"}) p.w.put(buckets.Wants, hkey, held) return p, said } @@ -727,7 +729,7 @@ func testAHeldModuleRaisesNoNeitherAndClearsAStandingOne(t *testing.T) { } // a standing neither this process raised p, said := setup() - p.neither = map[string]neitherOne{"shanks.systemd-resolved": {node: "shanks", module: "systemd-resolved", holding: assign.Holding{Rule: delivery.RuleNotHeld}}} + p.neither = map[string]neitherOne{"shanks.systemd-resolved": {node: "shanks", module: "systemd-resolved", holding: assign.Holding{Rule: mwc.RuleNotHeld}}} p.wants(context.Background()) p.wants(context.Background()) if r, c := neithers(*said); r != 0 || c != 1 { @@ -802,13 +804,13 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { if !c.noDigit { composes(t, p.w, "novox", digNew) } - heldOld, _ := delivery.HeldKey("web", fpOld) + heldOld, _ := mwc.HeldKey("web", fpOld) wantsKV.values[heldOld] = []byte("x") p.w.put(buckets.Wants, heldOld, []byte("x")) firstAsk, _ := decodeAsk(holdOld, asksKV.values[holdOld]) p.staleAsks(context.Background(), c.trunk) // the held record and condition of a fingerprint whose hold ask went are taken away with it - heldOld, _ = delivery.HeldKey("web", fpOld) + heldOld, _ = mwc.HeldKey("web", fpOld) _, heldThere := wantsKV.values[heldOld] cleared := slices.ContainsFunc(said, func(c oc.Condition) bool { return c.Key == assign.HeldCondition("web", fpOld, "", "", false, false).Key && c.State == occ.StateCleared @@ -955,8 +957,8 @@ func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) { p.w.put(buckets.Have, "g14.web", have) m := assign.Module{Name: "web", FP: fpTest, Nodes: []string{"g14"}, Self: func(string) bool { return true }, Release: func(fp string) (string, bool) { return p.w.release("web", fp) }} - out := assign.Outcome{Wants: map[string]delivery.Want{"g14": {FP: fpTest, Self: true}}} - p.yeses(context.Background(), "web", m, map[string]delivery.Want{"g14": {FP: fpOld, Self: true}}, &out) + out := assign.Outcome{Wants: map[string]mwc.Want{"g14": {Fp: fpTest, Self: true}}} + p.yeses(context.Background(), "web", m, map[string]mwc.Want{"g14": {Fp: fpOld, Self: true}}, &out) w := out.Wants["g14"] if got := w.Yes == waiting && w.Release == "feat/web"; got != c.yes || (!c.yes && (w.Yes != "" || w.Release != "")) { t.Errorf("%s: the want is %+v", c.name, w) @@ -1002,8 +1004,8 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) { put(buckets.Presence, "g14", on) have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Composed: was, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) put(buckets.Have, "g14.web", have) - wkey, _ := delivery.WantKey("g14", "web") - want, _ := delivery.EncodeWant(delivery.Want{FP: fpOld, Composed: was}) + wkey, _ := mwc.WantKey("g14", "web") + want, _ := mwc.EncodeWant(mwc.Want{Fp: fpOld, Composed: was}) wantsKV.values[wkey] = want put(buckets.Wants, wkey, want) sync := func() { @@ -1015,12 +1017,12 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) { p.wants(context.Background()) p.nodeWants(context.Background()) sync() - got, err := delivery.DecodeWant(wantsKV.values[wkey]) - if err != nil || got.FP != fpOld || got.Composed != "" || !got.Self { + got, err := mwc.DecodeWant(wantsKV.values[wkey]) + if err != nil || got.Fp != fpOld || got.Composed != "" || !got.Self { t.Fatalf("the placed node's want is %+v (%v), want it at %s, the node composing it", got, err, fpOld) } - nkey, _ := delivery.WantNodeKey("g14") - if nw, err := delivery.DecodeNodeWant(wantsKV.values[nkey]); err != nil || nw.Composed != "" || !nw.Self { + nkey, _ := mwc.WantNodeKey("g14") + if nw, err := mwc.DecodeNodeWant(wantsKV.values[nkey]); err != nil || nw.Composed != "" || !nw.Self { t.Fatalf("the placed node's own want is %+v (%v), want one naming no composition", nw, err) } @@ -1060,14 +1062,14 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) { put(buckets.Answers, akey, yes) p.wants(context.Background()) sync() - if got, err = delivery.DecodeWant(wantsKV.values[wkey]); err != nil || got.Yes != waiting || got.FP != fpOld { + if got, err = mwc.DecodeWant(wantsKV.values[wkey]); err != nil || got.Yes != waiting || got.Fp != fpOld { t.Fatalf("after the yes the want is %+v (%v), want it to name the yes %s", got, err, waiting) } // The node applied it: its have runs the composition; the yes stays while the want stays at its build. ran, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Composed: waiting, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) put(buckets.Have, "g14.web", ran) p.wants(context.Background()) - if got, _ = delivery.DecodeWant(wantsKV.values[wkey]); got.Yes != waiting { + if got, _ = mwc.DecodeWant(wantsKV.values[wkey]); got.Yes != waiting { t.Errorf("a pass after the move dropped the yes: %+v", got) } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go index 6de0ef97..9bf9d77b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -6,11 +6,13 @@ import ( "errors" "fmt" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/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" "mesh-delivery-assigner/internal/buckets" + "mesh-delivery-assigner/internal/puts" "sort" "strings" "sync" @@ -69,25 +71,25 @@ func newPutActs(bus caller, wake func(), logf func(string, ...any)) *putActs { } // identity names one put: its key and when it was asked, so a put asked again under the key is another. -func identity(key string, p delivery.Put) string { +func identity(key string, p puts.Put) string { return key + "@" + p.At.UTC().Format(time.RFC3339Nano) } // putsByModule is every put the assigner holds, by module then node. -func (p *passer) putsByModule() map[string]map[string]delivery.Put { - out := map[string]map[string]delivery.Put{} +func (p *passer) putsByModule() map[string]map[string]puts.Put { + out := map[string]map[string]puts.Put{} for key, value := range p.w.read(buckets.Puts) { - node, module, err := delivery.ParsePutKey(key) + node, module, err := puts.ParseKey(key) if err != nil { continue } - put, err := delivery.DecodePut(value) + put, err := puts.Decode(value) if err != nil { p.logf("the put %s cannot be read, and is left: %v", key, err) continue } if out[module] == nil { - out[module] = map[string]delivery.Put{} + out[module] = map[string]puts.Put{} } out[module][node] = put } @@ -120,29 +122,29 @@ func (p *passer) puts(ctx context.Context) { } } for node, put := range all[module] { - if delivery.PutEnded(put.Step) { + if puts.Ended(put.Step) { continue } - key, _ := delivery.PutKey(node, module) + key, _ := puts.Key(node, module) id := identity(key, put) world := p.putWorld(node, module, put, manifest, assigned[node]) world.Snapshot, world.Snapshotting, world.SnapshotFailed = p.acts.state(id) next := assign.StepPut(world) // A held bus whose user list the rules moved on (ADR 0539 §7): the put follows the composition its want names. - if w, ok := wanted[module][node]; ok && w.Put && w.FP == next.Put.FP && w.Composed != "" && w.Composed != next.Put.Composed && - (next.Put.Step == delivery.PutWanted || next.Put.Step == delivery.PutHolding) { - if d, _ := p.composedFor(node, module, w.FP); d != nil && d.UserListOnly && d.Digest == w.Composed { + if w, ok := wanted[module][node]; ok && w.Put && w.Fp == next.Put.FP && w.Composed != "" && w.Composed != next.Put.Composed && + (next.Put.Step == puts.Wanted || next.Put.Step == puts.Holding) { + if d, _ := p.composedFor(node, module, w.Fp); d != nil && d.UserListOnly && d.Digest == w.Composed { next.Put = next.Put.Said(world.Now, next.Put.Step, fmt.Sprintf("the bus's user list changed: the want names composition %s", shortHex(w.Composed))) next.Put.Composed = w.Composed } } // The bus's want is written only for the act the operator's yes bound (ADR 0546 clause 8). - if next.Put.Step == delivery.PutWanted && put.Step != delivery.PutWanted && world.Bus && next.Put.Ask != "" { + if next.Put.Step == puts.Wanted && put.Step != puts.Wanted && world.Bus && next.Put.Ask != "" { if err := p.performs(next.Put.Ask, assign.PutAct(node, module, next.Put.FP, next.Put.Composed)); err != nil { - next = assign.PutNext{Put: next.Put.Said(world.Now, delivery.PutRefused, "the bus's want is not written: "+err.Error())} + next = assign.PutNext{Put: next.Put.Said(world.Now, puts.Refused, "the bus's want is not written: "+err.Error())} } } - if failed := p.acts.warrantFailed(id); failed != "" && (next.Put.Step == delivery.PutWanted || next.Put.Step == delivery.PutHolding) && + if failed := p.acts.warrantFailed(id); failed != "" && (next.Put.Step == puts.Wanted || next.Put.Step == puts.Holding) && !strings.Contains(next.Put.Words, failed) { next.Put = next.Put.Said(world.Now, next.Put.Step, next.Put.Words+"; "+failed) } @@ -151,12 +153,12 @@ func (p *passer) puts(ctx context.Context) { if !written { continue // released or put again meanwhile: nothing is done for the put that no longer stands } - if next.Put.Step == delivery.PutWanted && put.Step != delivery.PutWanted { + if next.Put.Step == puts.Wanted && put.Step != puts.Wanted { // The want is written in this pass too, before the next one would: Module.Puts writes the same. - if wkey, err := delivery.WantKey(node, module); err == nil { + if wkey, err := mwc.WantKey(node, module); err == nil { want := assign.PutWant(next.Put) - if before, ok := wanted[module][node]; !ok || !delivery.SameWant(normalWant(before), normalWant(want)) { - p.put(ctx, buckets.Wants, wkey, func() ([]byte, error) { return delivery.EncodeWant(want) }) + if before, ok := wanted[module][node]; !ok || !mwc.SameWant(normalWant(before), normalWant(want)) { + p.put(ctx, buckets.Wants, wkey, func() ([]byte, error) { return mwc.EncodeWant(want) }) } } if next.Warranted { @@ -167,13 +169,13 @@ func (p *passer) puts(ctx context.Context) { } } -func normalWant(w delivery.Want) delivery.Want { +func normalWant(w mwc.Want) mwc.Want { w.V = 0 return w } // putWorld is what one step of a put reads from the world. -func (p *passer) putWorld(node, module string, put delivery.Put, manifest catalogue.Manifest, assignedHere []string) assign.PutWorld { +func (p *passer) putWorld(node, module string, put puts.Put, manifest catalogue.Manifest, assignedHere []string) assign.PutWorld { w := assign.PutWorld{Node: node, Module: module, Put: put, Now: p.clock(), Bus: catalogue.ProvidesBus(manifest), Newest: p.w.trunk()[module].Fp2} for _, m := range assignedHere { if m == module { @@ -245,7 +247,7 @@ func (p *passer) composedFor(node, module, fp string) (*assign.ComposedDigest, b // actOn does the acts a step asks for. func (p *passer) actOn(ctx context.Context, node, module, id string, w assign.PutWorld, next assign.PutNext) { - if next.Put.Step == delivery.PutPerson && next.Put.Ask != "" && w.Composed != nil { + if next.Put.Step == puts.Person && next.Put.Ask != "" && w.Composed != nil { // The put's own ask, written again over one an earlier put left under the key (its asking differs), so the teller // raises it anew under its own id. akey := next.Put.Ask @@ -279,13 +281,13 @@ func (p *passer) performs(key string, act occ.Act) error { } // samePut says two puts say the same: their step, words and what they found. -func samePut(a, b delivery.Put) bool { +func samePut(a, b puts.Put) bool { return a.Step == b.Step && a.Words == b.Words && a.Composed == b.Composed && a.Ask == b.Ask && a.Snapshot == b.Snapshot } // writePut writes a put's next value over the one the pass read, by compare-and-set, unless the tool has put again or // released it since; and says it as the event put. It says whether it wrote. -func (p *passer) writePut(ctx context.Context, key string, read, next delivery.Put) bool { +func (p *passer) writePut(ctx context.Context, key string, read, next puts.Put) bool { kv := p.kvs[buckets.Puts] if kv == nil { return false @@ -295,11 +297,11 @@ func (p *passer) writePut(ctx context.Context, key string, read, next delivery.P p.logf("the put %s cannot be read again, and is taken further at the next pass: %v", key, err) return false } - now, err := delivery.DecodePut(e.Value()) + now, err := puts.Decode(e.Value()) if err != nil || !now.At.Equal(read.At) || now.Step != read.Step { return false // asked again or released meanwhile: the next pass reads that } - data, err := delivery.EncodePut(next) + data, err := puts.Encode(next) if err != nil { p.logf("the put %s is not written: %v", key, err) return false @@ -435,8 +437,8 @@ type putSaid struct { } // say says a put's step as the event put, once per step and words. -func (a *putActs) say(key string, p delivery.Put) { - node, module, err := delivery.ParsePutKey(key) +func (a *putActs) say(key string, p puts.Put) { + node, module, err := puts.ParseKey(key) if err != nil || a.bus == nil { return } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go index cffd4a5e..94219f87 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go @@ -6,6 +6,7 @@ import ( "fmt" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "mesh-delivery-assigner/internal/puts" "os" "regexp" "slices" @@ -122,14 +123,14 @@ func (s *putTools) version(module, version string) (string, error) { type putAnswer struct { Node string `json:"node"` Module string `json:"module"` - delivery.Put + puts.Put Follow string `json:"follow,omitempty"` } // doPut is the tool assigner_put. func (s *putTools) doPut(args map[string]any, by string) (any, error) { node, module, why := arg(args, "node"), arg(args, "module"), arg(args, "why") - key, err := delivery.PutKey(node, module) + key, err := puts.Key(node, module) if err != nil { return nil, err } @@ -153,15 +154,15 @@ func (s *putTools) doPut(args map[string]any, by string) (any, error) { if raw, _, found, err := s.st.get(putsState, key); err != nil { return nil, fmt.Errorf("the put of %s on %s cannot be read: %w", module, node, err) } else if found { - if before, err := delivery.DecodePut(raw); err == nil && !delivery.PutEnded(before.Step) { + if before, err := puts.Decode(raw); err == nil && !puts.Ended(before.Step) { if before.FP == fp && before.Hold == hold { return putAnswer{Node: node, Module: module, Put: before, Follow: followPut(node, module)}, nil } words += fmt.Sprintf("; it replaces the put of %s at %s, which stood at %s", module, shortFP(before.FP), before.Step) } } - p := delivery.Put{FP: fp, Hold: hold, Why: why, By: by, At: now}.Said(now, delivery.PutAsked, words) - data, err := delivery.EncodePut(p) + p := puts.Put{FP: fp, Hold: hold, Why: why, By: by, At: now}.Said(now, puts.Asked, words) + data, err := puts.Encode(p) if err != nil { return nil, err } @@ -173,15 +174,15 @@ func (s *putTools) doPut(args map[string]any, by string) (any, error) { } // await is the put at key once the assigner's process took it up past its asking, or as it stands after answerWithin. -func (s *putTools) await(key string, p delivery.Put) delivery.Put { +func (s *putTools) await(key string, p puts.Put) puts.Put { deadline := time.After(answerWithin) for { s.mu.Lock() raw, changed := s.heard[key], s.changed s.mu.Unlock() - if now, err := delivery.DecodePut(raw); err == nil && now.At.Equal(p.At) { + if now, err := puts.Decode(raw); err == nil && now.At.Equal(p.At) { p = now - if now.Step != delivery.PutAsked && now.Step != delivery.PutBuild && now.Step != delivery.PutCompose { + if now.Step != puts.Asked && now.Step != puts.Build && now.Step != puts.Compose { return p // it waits on a person or the node, or ended } } @@ -202,7 +203,7 @@ func followPut(node, module string) string { // doRelease is the tool assigner_release. func (s *putTools) doRelease(args map[string]any, by string) (any, error) { node, module, why := arg(args, "node"), arg(args, "module"), arg(args, "why") - key, err := delivery.PutKey(node, module) + key, err := puts.Key(node, module) if err != nil { return nil, err } @@ -216,15 +217,15 @@ func (s *putTools) doRelease(args map[string]any, by string) (any, error) { if !found { return nil, fmt.Errorf("%s holds no put of %s: nothing to release", node, module) } - p, err := delivery.DecodePut(raw) + p, err := puts.Decode(raw) if err != nil { return nil, fmt.Errorf("the put of %s on %s cannot be read: %w", module, node, err) } - if delivery.PutEnded(p.Step) { + if puts.Ended(p.Step) { return nil, fmt.Errorf("the put of %s on %s ended already (%s: %s): nothing to release", module, node, p.Step, p.Words) } - p = p.Said(s.now(), delivery.PutReleased, fmt.Sprintf("released by %s: %s; the assigner's rules write the want again", by, why)) - data, err := delivery.EncodePut(p) + p = p.Said(s.now(), puts.Released, fmt.Sprintf("released by %s: %s; the assigner's rules write the want again", by, why)) + data, err := puts.Encode(p) if err != nil { return nil, err } @@ -244,7 +245,7 @@ func (s *putTools) doPuts(args map[string]any) (any, error) { sort.Strings(keys) out := []putAnswer{} for _, key := range keys { - node, module, err := delivery.ParsePutKey(key) + node, module, err := puts.ParseKey(key) if err != nil || (arg(args, "node") != "" && node != arg(args, "node")) || (arg(args, "module") != "" && module != arg(args, "module")) { continue } @@ -252,7 +253,7 @@ func (s *putTools) doPuts(args map[string]any) (any, error) { if err != nil || !found { continue } - p, err := delivery.DecodePut(raw) + p, err := puts.Decode(raw) if err != nil { continue } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go index d622a712..6376020e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go @@ -10,6 +10,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "mesh-delivery-assigner/internal/buckets" + "mesh-delivery-assigner/internal/puts" "sort" "strconv" "time" @@ -296,13 +297,13 @@ func (s *wantsTools) doBus(map[string]any) (any, error) { } else { out["steps"] = steps } - puts, err := s.values(putsState) + kept, err := s.values(putsState) if err != nil { out["puts-unread"] = err.Error() } else { mine := map[string]json.RawMessage{} - for k, v := range puts { - if _, module, err := delivery.ParsePutKey(k); err == nil && module == bus { + for k, v := range kept { + if _, module, err := puts.ParseKey(k); err == nil && module == bus { mine[k] = v } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 0140c9e5..0b216c04 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -6,6 +6,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + 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/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" @@ -356,19 +357,19 @@ func (w *world) online() map[string]bool { } // wantsOf is the wants the assigner wrote, by module then node. -func (w *world) wantsOf() map[string]map[string]delivery.Want { - out := map[string]map[string]delivery.Want{} +func (w *world) wantsOf() map[string]map[string]mwc.Want { + out := map[string]map[string]mwc.Want{} for key, value := range w.read(buckets.Wants) { - node, module, err := delivery.ParseWantKey(key) + node, module, err := mwc.ParseWantKey(key) if err != nil { continue } - want, err := delivery.DecodeWant(value) + want, err := mwc.DecodeWant(value) if err != nil { continue } if out[module] == nil { - out[module] = map[string]delivery.Want{} + out[module] = map[string]mwc.Want{} } out[module][node] = want } diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 489a00ed..2161515c 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -62,3 +62,5 @@ 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 diff --git a/modules/mesh-delivery-assigner/internal/assign/asks.go b/modules/mesh-delivery-assigner/internal/assign/asks.go index 8ae4b757..5fbbeea1 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks.go @@ -2,6 +2,7 @@ package assign import ( "fmt" + 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" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" @@ -152,8 +153,8 @@ func LiftedCondition(module, fp string) oc.Condition { const ( OptionYes = "yes" OptionNo = "no" - OptionBackupAgain = delivery.BackupAgain - OptionBackupSkip = delivery.BackupSkip + OptionBackupAgain = mwc.BackupAgain + OptionBackupSkip = mwc.BackupSkip ) // RecordAsk is the ask about a move of a module of policy record on node to the composition with the digest, judged by the diff --git a/modules/mesh-delivery-assigner/internal/assign/asks_test.go b/modules/mesh-delivery-assigner/internal/assign/asks_test.go index e16446f7..888ce7ec 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks_test.go @@ -2,10 +2,12 @@ package assign import ( "fmt" + 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" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" + "mesh-delivery-assigner/internal/puts" "slices" "strings" "testing" @@ -203,7 +205,7 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { if !strings.Contains(mustAsk(t, r).Explanation, "gone") || mustAsk(t, r).Options[0].ID != OptionYes { t.Errorf("the record ask = %+v", mustAsk(t, r)) } - if !strings.Contains(mustAsk(t, b).Explanation, "no space left") || mustAsk(t, b).Options[0].ID != delivery.BackupAgain || mustAsk(t, b).Options[1].ID != delivery.BackupSkip { + if !strings.Contains(mustAsk(t, b).Explanation, "no space left") || mustAsk(t, b).Options[0].ID != mwc.BackupAgain || mustAsk(t, b).Options[1].ID != mwc.BackupSkip { t.Errorf("the backup ask = %+v", mustAsk(t, b)) } // the id binds the ask to the composition and to its number (review round 1, items 3 and 4) @@ -225,7 +227,7 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { } // ADR 0546 clauses 7 and 8: a put's ask is valid at the record ask's key; for the bus its yes approves and binds the act, // so the view cannot answer it, and it lasts no longer than an approving ask may. - put := delivery.Put{FP: fNew, Why: "the bus's user list", Hold: true} + put := puts.Put{FP: fNew, Why: "the bus's user list", Hold: true} d := ComposedDigest{Digest: dgst, Judgement: JudgedCannot, Words: "the have names no composition"} for _, bus := range []bool{false, true} { a, err := PutAsk(rkey, "ace", "mail", put, d, bus, now) @@ -273,7 +275,7 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { // 1945/AC21: the condition of a module on neither list is urgent, names the module, the node and the rule holding its want back, // under the assigner's own name keyed by node and module, and is cleared under the same key. func TestTheNeitherConditionNamesModuleNodeAndRule(t *testing.T) { - c := NeitherCondition("g14", "i3status-rust", Holding{Rule: delivery.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}, true) + c := NeitherCondition("g14", "i3status-rust", Holding{Rule: mwc.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}, true) if _, err := occ.EncodeCondition(c); err != nil { t.Fatalf("the condition is refused: %v", err) } diff --git a/modules/mesh-delivery-assigner/internal/assign/put.go b/modules/mesh-delivery-assigner/internal/assign/put.go index f91672e2..9b3693f1 100644 --- a/modules/mesh-delivery-assigner/internal/assign/put.go +++ b/modules/mesh-delivery-assigner/internal/assign/put.go @@ -4,14 +4,15 @@ 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" "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" "time" 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-sdk/go/delivery" ) // A put (novox/hq ADR 0546): one version of a module put on one node by the assigner's tool assigner_put. The tool writes the put @@ -25,7 +26,7 @@ const ComposeWithin = 10 * time.Minute // PutWorld is what one step of a put reads. type PutWorld struct { Node, Module string - Put delivery.Put + Put puts.Put // Assigned says the module stands assigned to the node. Assigned bool // Bus says the module provides the mesh's bus (ADR 0539 §15); Recorded that it is of policy record (§14). @@ -54,7 +55,7 @@ type PutWorld struct { // PutNext is what a step decides: the put as it stands after it, and the acts the pass does for it. type PutNext struct { - Put delivery.Put + Put puts.Put // Ask writes the put's ask about the move (PutAsk). Ask bool // Snapshot has the bus's node back the bus up now, its dump the streams' snapshot (ADR 0235). @@ -66,7 +67,7 @@ type PutNext struct { // StepPut takes a put one step further from what the world holds (ADR 0546 clauses 3 to 8). func StepPut(w PutWorld) PutNext { p := w.Put - if delivery.PutEnded(p.Step) { + if puts.Ended(p.Step) { return PutNext{Put: p} } say := func(step, format string, args ...any) PutNext { @@ -74,21 +75,21 @@ func StepPut(w PutWorld) PutNext { } fp := short(p.FP) if !w.Assigned { - return say(delivery.PutRefused, "%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 placement first)", w.Module, w.Node) } switch w.Build { case builds.BuildDone: case builds.BuildFailed: - return say(delivery.PutRefused, "the builder's build of %s at %s failed: %s", w.Module, fp, w.BuildWords) + return say(puts.Refused, "the builder's build of %s at %s failed: %s", w.Module, fp, w.BuildWords) case "": if p.FP != w.Newest { // The builder builds the trunk's newest fingerprint and the candidates', and takes no ask for another yet (ADR 0546 // clause 3): an older fingerprint it never built, or whose key it removed, cannot be put. - return say(delivery.PutRefused, "the builder holds no build of %s at %s, and builds by itself only the trunk's newest (%s): put that, or a version it built", w.Module, fp, short(w.Newest)) + return say(puts.Refused, "the builder holds no build of %s at %s, and builds by itself only the trunk's newest (%s): put that, or a version it built", w.Module, fp, short(w.Newest)) } - return say(delivery.PutBuild, "waits for the builder's build of %s at %s, the trunk's newest, which it builds by itself", w.Module, fp) + return say(puts.Build, "waits for the builder's build of %s at %s, the trunk's newest, which it builds by itself", w.Module, fp) default: - return say(delivery.PutBuild, "waits for the builder's build of %s at %s, which is %s", w.Module, fp, w.Build) + return say(puts.Build, "waits for the builder's build of %s at %s, which is %s", w.Module, fp, w.Build) } if w.Composed == nil || !w.Granted { missing := fmt.Sprintf("no composition of %s for %s at %s is written", w.Module, w.Node, fp) @@ -96,30 +97,30 @@ func StepPut(w PutWorld) PutNext { missing = fmt.Sprintf("the grant of %s at %s does not name %s's accounts", w.Module, fp, w.Node) } since := w.Now - if p.Step == delivery.PutCompose && len(p.Steps) > 0 { + if p.Step == puts.Compose && len(p.Steps) > 0 { since = p.Steps[len(p.Steps)-1].At } if w.Now.Sub(since) > ComposeWithin { - return say(delivery.PutRefused, "%s after %s: only a composition written by the take-in of its build (or one of the five kept) is put; "+ + return say(puts.Refused, "%s after %s: only a composition written by the take-in of its build (or one of the five kept) is put; "+ "for another the hand path is mesh-hand-compose (ADR 0539 §10)", missing, ComposeWithin) } - return say(delivery.PutCompose, "waits up to %s: %s yet", ComposeWithin, missing) + return say(puts.Compose, "waits up to %s: %s yet", ComposeWithin, missing) } d := *w.Composed - if p.Step == delivery.PutWanted || p.Step == delivery.PutHolding { + if p.Step == puts.Wanted || p.Step == puts.Holding { return PutNext{Put: applied(w, p)} } p.Composed = d.Digest if needsPerson(w, d) { switch w.Answer { case OptionNo: - return say(delivery.PutRefused, "the operator answered no to putting %s at %s on %s", w.Module, fp, w.Node) + return say(puts.Refused, "the operator answered no to putting %s at %s on %s", w.Module, fp, w.Node) case OptionYes: default: if w.AskExpired { - return say(delivery.PutRefused, "the operator's ask about putting %s at %s on %s expired unanswered: the node keeps what it runs", w.Module, fp, w.Node) + return say(puts.Refused, "the operator's ask about putting %s at %s on %s expired unanswered: the node keeps what it runs", w.Module, fp, w.Node) } - next := say(delivery.PutPerson, "waits for the operator's yes to putting %s at %s on %s (composition %s, judged %s)", w.Module, fp, w.Node, short(d.Digest), d.Judgement) + next := say(puts.Person, "waits for the operator's yes to putting %s at %s on %s (composition %s, judged %s)", w.Module, fp, w.Node, short(d.Digest), d.Judgement) if p.Ask == "" { next.Ask = true next.Put.Ask, _ = asks.RecordAskKey(w.Node, w.Module, d.Digest) @@ -131,11 +132,11 @@ func StepPut(w PutWorld) PutNext { case w.Snapshot != "": p.Snapshot = w.Snapshot case w.SnapshotFailed != "": - return say(delivery.PutRefused, "the bus's streams were not snapshotted, so the bus is not put: %s", w.SnapshotFailed) + return say(puts.Refused, "the bus's streams were not snapshotted, so the bus is not put: %s", w.SnapshotFailed) case w.Snapshotting: - return say(delivery.PutSnapshot, "the operator said yes; waits for %s's backup holder to snapshot the bus's streams", w.Node) + return say(puts.Snapshot, "the operator said yes; waits for %s's backup holder to snapshot the bus's streams", w.Node) default: - next := say(delivery.PutSnapshot, "the operator said yes; asked %s's backup holder to snapshot the bus's streams", w.Node) + next := say(puts.Snapshot, "the operator said yes; asked %s's backup holder to snapshot the bus's streams", w.Node) next.Snapshot = true return next } @@ -148,22 +149,22 @@ func StepPut(w PutWorld) PutNext { if p.Snapshot != "" { words += "; the streams were snapshotted: " + p.Snapshot } - next := PutNext{Put: applied(w, p.Said(w.Now, delivery.PutWanted, words))} + next := PutNext{Put: applied(w, p.Said(w.Now, puts.Wanted, words))} next.Warranted = w.Bus && p.Ask != "" return next } // applied is a wanted put once the node's have says how it went: holding or done when it runs the composition healthy. -func applied(w PutWorld, p delivery.Put) delivery.Put { +func applied(w PutWorld, p puts.Put) puts.Put { h := w.Have if h == nil || h.Fp != p.FP || h.Composed != p.Composed { return p } switch { case h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK && p.Hold: - return p.Said(w.Now, delivery.PutHolding, fmt.Sprintf("%s runs %s at %s healthy; held there until released", w.Node, w.Module, short(p.FP))) + return p.Said(w.Now, puts.Holding, fmt.Sprintf("%s runs %s at %s healthy; held there until released", w.Node, w.Module, short(p.FP))) case h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK: - return p.Said(w.Now, delivery.PutDone, fmt.Sprintf("%s runs %s at %s healthy; the rules write its want again", w.Node, w.Module, short(p.FP))) + return p.Said(w.Now, puts.Done, fmt.Sprintf("%s runs %s at %s healthy; the rules write its want again", w.Node, w.Module, short(p.FP))) case failed(*h): return p.Said(w.Now, p.Step, fmt.Sprintf("%s runs %s at %s and its health check failed: %s", w.Node, w.Module, short(p.FP), h.Detail)) } @@ -188,18 +189,18 @@ func needsPerson(w PutWorld, d ComposedDigest) bool { } // PutWant is the want a put writes once it is wanted: the fingerprint and composition, marked put, and held when it holds. -func PutWant(p delivery.Put) delivery.Want { - return delivery.Want{FP: p.FP, Composed: p.Composed, Put: true, Hold: p.Hold, Words: "put: " + p.Why} +func PutWant(p puts.Put) mwc.Want { + return mwc.Want{Fp: p.FP, Composed: p.Composed, Put: true, Hold: p.Hold, Words: "put: " + p.Why} } // PutOwns says what a put does to the node's want: owns says the rules pass over it, want the want it writes (nil: what // stands stays). A put owns the want from its asking until it ends; a held put ends only when released. -func PutOwns(p delivery.Put) (want *delivery.Want, owns bool) { +func PutOwns(p puts.Put) (want *mwc.Want, owns bool) { switch p.Step { - case delivery.PutWanted, delivery.PutHolding: + case puts.Wanted, puts.Holding: w := PutWant(p) return &w, true - case delivery.PutDone, delivery.PutReleased, delivery.PutRefused: + case puts.Done, puts.Released, puts.Refused: return nil, false } return nil, true @@ -208,7 +209,7 @@ func PutOwns(p delivery.Put) (want *delivery.Want, owns bool) { // PutAsking is a put's own asking number: when it was asked, in milliseconds. Each put asked under the same key is another // asking, so the ask is written again under an id of its own and an answer to an earlier put, whose warrant names that put's // asking, never counts for a later one (ADR 0546 clause 8: one approval binds one act). -func PutAsking(p delivery.Put) int { +func PutAsking(p puts.Put) int { if n := p.At.UnixMilli(); n > 0 { return int(n) } @@ -223,7 +224,7 @@ func PutAct(node, module, fp, composed string) occ.Act { // PutAsk is the ask about a put's move (ADR 0546 clauses 7 and 8), under the record ask's key of the composition so the // teller relays it and its answer as any record ask's. For the bus its yes is of level approve and binds the act, so only a // channel that proves who answered takes it (ADR 0259), never the view. -func PutAsk(key, node, module string, p delivery.Put, d ComposedDigest, bus bool, now time.Time) (asks.Ask, error) { +func PutAsk(key, node, module string, p puts.Put, d ComposedDigest, bus bool, now time.Time) (asks.Ask, error) { yes := oc.Option{ID: OptionYes, Label: "Yes, put it", Does: "the node is told to apply " + module + " at " + short(p.FP), Level: occ.Acknowledge} what := map[Judgement]string{ JudgedRecreates: "recreates a container", diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 092ded84..a4aadc29 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -4,16 +4,17 @@ 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" "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "maps" + "mesh-delivery-assigner/internal/puts" "slices" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-delivery/sdk/forms" @@ -81,7 +82,7 @@ type Module struct { // Now is when this pass reads the haves: the held record names it as when the health check was seen to fail. Zero: not said. Now time.Time // Puts are the puts of the module, by node (ADR 0546): while one owns a node's want (PutOwns), the rules pass over it. - Puts map[string]delivery.Put + Puts map[string]puts.Put // Needs are the reader capabilities the manifest at FP needs (the indexer's modules value, catalogue.ManifestNeeds, ADR // 0614 §4): a node's want moves only while that node's engine states each of them. Nil: none. Needs []string @@ -146,40 +147,40 @@ func Holds(m Module, node string, n Nodes) (bool, int, string) { func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) { switch { case !m.Built: - return false, delivery.RuleBuildDone, fmt.Sprintf("the build of %s at %s is not done", m.Name, short(m.FP)) + return false, mwc.RuleBuildDone, fmt.Sprintf("the build of %s at %s is not done", m.Name, short(m.FP)) case m.Held: - return false, delivery.RuleNotHeld, fmt.Sprintf("%s is held at %s", m.Name, short(m.FP)) + return false, mwc.RuleNotHeld, fmt.Sprintf("%s is held at %s", m.Name, short(m.FP)) } if ordering { canary := Canary(m.Nodes, n.Online) switch { case canary == "": - return false, delivery.RuleCanary, fmt.Sprintf("none of the nodes of %s is online, so it has no canary", m.Name) + return false, mwc.RuleCanary, fmt.Sprintf("none of the nodes of %s is online, so it has no canary", m.Name) case node != canary && !healthy(n.Haves[canary][m.Name], m.FP): - return false, delivery.RuleCanary, fmt.Sprintf("the canary %s does not run %s healthy yet", canary, short(m.FP)) + return false, mwc.RuleCanary, fmt.Sprintf("the canary %s does not run %s healthy yet", canary, short(m.FP)) } } if !m.Granted { - return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP)) + return false, mwc.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP)) } if m.GrantedOn != nil && !m.GrantedOn(node) { - return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s on %s for %s", m.Name, node, + return false, mwc.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s on %s for %s", m.Name, node, short(m.FP)) } if words, lacking := m.readerLacking(node, n); lacking { - return false, delivery.RuleComposed, words + return false, mwc.RuleComposed, words } if _, ok := m.composition(node, m.FP); !ok { - return false, delivery.RuleComposed, fmt.Sprintf("%s has named no composition of its own for %s at %s yet", node, m.Name, short(m.FP)) + return false, mwc.RuleComposed, fmt.Sprintf("%s has named no composition of its own for %s at %s yet", node, m.Name, short(m.FP)) } if words, waiting := m.provisionWaiting(node); waiting { - return false, delivery.RuleProvisions, words + return false, mwc.RuleProvisions, words } if words, waiting := m.verbWaiting(node); waiting { - return false, delivery.RuleProvisions, words + return false, mwc.RuleProvisions, words } if words, waiting := m.busStateWaiting(node); waiting { - return false, delivery.RuleProvisions, words + return false, mwc.RuleProvisions, words } return true, 0, "" } @@ -324,9 +325,9 @@ func keepsData(m catalogue.Manifest) bool { // Outcome is what one module's pass decides: each node's want to write (only those that change), and a hold to write. type Outcome struct { - Wants map[string]delivery.Want + Wants map[string]mwc.Want // Held is the hold of m.FP to write, when a node's health check failed on it and it is not held yet. - Held *delivery.Held + Held *mwc.Held // Lifted is the hold of m.FP lifted by itself: every node that runs the module runs m.FP healthy (issue 1906). The held // record and its ask are taken away, and the wants move by the rules again in this pass. Lifted bool @@ -376,8 +377,8 @@ func (h Holding) Reason() string { // health check of m.FP on a node holds it: every node wanting m.FP is set back to its last healthy fingerprint, but a // module holding data is not set back but asked about (Restore). A // node that wants m but is no longer among its nodes gets its want written removed, never deleted (ADR 0408 §4). -func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[string]string) Outcome { - out := Outcome{Wants: map[string]delivery.Want{}, Rules: map[string]Holding{}, Neither: map[string]Holding{}} +func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string]string) Outcome { + out := Outcome{Wants: map[string]mwc.Want{}, Rules: map[string]Holding{}, Neither: map[string]Holding{}} held := m.Held if held && !m.Kept && m.FP != "" && allRunHealthy(m, n) { out.Lifted, held = true, false @@ -400,7 +401,7 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st out.NodeFailed[node] = NodeFailure{Others: others, Words: HeldWords(h, time.Time{})} // said once: no clock in it continue } - out.Held = &delivery.Held{Node: node, Words: HeldWords(h, m.Now)} + out.Held = &mwc.Held{Node: node, Words: HeldWords(h, m.Now)} held = true break } @@ -412,24 +413,24 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st break } before, had := wants[node] - var next delivery.Want + var next mwc.Want p, put := m.Puts[node] if !put && had && before.Put && before.Hold { // The want is the truth (ADR 0546 clause 5): a want marked put and held stands while its put is not heard (the // puts not open, or not yet handed after a start); only a put heard released gives it back to the rules. A put // without hold is not kept this way: its put takes it further once heard. - p, put = delivery.Put{FP: before.FP, Composed: before.Composed, Hold: true, Why: strings.TrimPrefix(before.Words, "put: "), - Step: delivery.PutHolding}, true + p, put = puts.Put{FP: before.Fp, Composed: before.Composed, Hold: true, Why: strings.TrimPrefix(before.Words, "put: "), + Step: puts.Holding}, true } if put { if want, owns := PutOwns(p); owns { if want != nil { w := *want // A held bus still takes a revision of its composition that changes only its user list (ADR 0539 §7). - if ul, ok := m.userListRevision(node, before, had, n); ok && ul.FP == w.FP { + if ul, ok := m.userListRevision(node, before, had, n); ok && ul.Fp == w.Fp { w.Composed = ul.Composed } - if !had || !delivery.SameWant(normal(before), normal(w)) { + if !had || !mwc.SameWant(normal(before), normal(w)) { out.Wants[node] = w } } @@ -440,14 +441,14 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st // node runs, outside the rules, the record hold and the bar on a new fingerprint: a new module's credential must reach the // bus before the module can connect (ADR 0539 §7). The controller marks it; whatever it judges of the file's mount stands. if w, ok := m.userListRevision(node, before, had, n); ok { - if !had || !delivery.SameWant(normal(before), normal(w)) { + if !had || !mwc.SameWant(normal(before), normal(w)) { out.Wants[node] = w } continue } ok, rule, words := Holds(mm, node, n) target := m.FP - if have, ran := n.Haves[node][m.Name]; !ok && (!had || before.Removed) && rule == delivery.RuleCanary { + if have, ran := n.Haves[node][m.Name]; !ok && (!had || before.Removed) && rule == mwc.RuleCanary { // The canary orders a move, never the first want of a node that does not run the module (issue 1960: de-spiegel, // link2pay and photos on novox were left with no want). A node that runs an older fingerprint of the new form, // composed and granted, gets its first want at what it runs, and rule 3 then orders its move to F as from any want. @@ -475,32 +476,32 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st } out.Unreached[node] = why } - if ok && target == m.FP && len(out.NodeFailed) > 0 && !(had && !before.Removed && before.FP == m.FP) && + if ok && target == m.FP && len(out.NodeFailed) > 0 && !(had && !before.Removed && before.Fp == m.FP) && n.Haves[node][m.Name].Fp != m.FP { // While a node fails F alone, F reaches no further node: a node that neither wants nor runs F gets no new want // naming it, and wants already written stay (ADR 0557 §2). A node that runs F with no live want gets its want at F, or // it would be on neither list and its node-engine would give the module up (issue 1945). - ok, rule, words = false, delivery.RuleNotHeld, FailedAloneWords(m.Name, m.FP, slices.Sorted(maps.Keys(out.NodeFailed))) + ok, rule, words = false, mwc.RuleNotHeld, FailedAloneWords(m.Name, m.FP, slices.Sorted(maps.Keys(out.NodeFailed))) } switch { case ok: d, _ := m.composition(node, target) - next = delivery.Want{FP: target, Composed: d.Digest} + next = mwc.Want{Fp: target, Composed: d.Digest} at := m if target != m.FP { // the first want at the older fingerprint the node runs: held back from F by the rule, and judged as a want at it - next.Newest, next.Rule, next.Words = m.FP, rule, words + next.Newest, next.Rule, next.Words = m.FP, int64(rule), words at.FP = target } // A placed node's want names the build alone: it is the same target while the build is. - if had && !before.Removed && !before.Put && before.FP == target && (before.Composed == d.Digest || (before.Self && m.selfOn(node))) { + if had && !before.Removed && !before.Put && before.Fp == target && (before.Composed == d.Digest || (before.Self && m.selfOn(node))) { next.Backup, next.Words = before.Backup, before.Words // the answer to a failed backup stays while its target stays (ADR 0539 §16) } running, ran := n.Haves[node][m.Name] // A removed want is no want: a first want after it goes through the record ask as a first want does. asked, hadLive := before, had if before.Removed { - asked, hadLive = delivery.Want{}, false + asked, hadLive = mwc.Want{}, false } if need, wait := at.mustAsk(node, asked, hadLive, ran, running.Fp, running.Composed == "", d); wait { if need != nil { @@ -508,7 +509,7 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st } continue } - case held && had && before.FP == m.FP: + case held && had && before.Fp == m.FP: out.Rules[node] = Holding{Rule: rule, Words: words} last := lastHealthy[node] switch { @@ -526,7 +527,7 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st next.Words = fmt.Sprintf("%s is held at %s, and the composition of %s at %s is gone, so %s is not set back", m.Name, short(m.FP), m.Name, short(last), node) break } - next = delivery.Want{FP: last, Composed: d.Digest, Newest: m.FP, Rule: rule, Words: words} + next = mwc.Want{Fp: last, Composed: d.Digest, Newest: m.FP, Rule: int64(rule), Words: words} } default: out.Rules[node] = Holding{Rule: rule, Words: words} @@ -543,23 +544,23 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st // A removed want of a node that does not run the module says why no want follows it (issue 1945), so the view's // assigner_want answers it; it stays removed. A node with no key gets none: a removed want written where none was // would have its node agent remove the module and stop holding its composition's resources back. - next = delivery.Want{Removed: true, Words: NoWantWords(rule, words)} - if !delivery.SameWant(normal(before), normal(next)) { + next = mwc.Want{Removed: true, Words: NoWantWords(rule, words)} + if !mwc.SameWant(normal(before), normal(next)) { out.Wants[node] = next } } continue } - next = delivery.Want{FP: before.FP, Composed: before.Composed, Newest: m.FP, Rule: rule, Words: words} - if before.FP == m.FP { + next = mwc.Want{Fp: before.Fp, Composed: before.Composed, Newest: m.FP, Rule: int64(rule), Words: words} + if before.Fp == m.FP { // Held at F by the rules: a new revision of F's composition waits for them, as a new fingerprint does (ADR 0539 §5). - next = delivery.Want{FP: m.FP, Composed: before.Composed} - } else if d, there := m.composition(node, before.FP); there && d.Digest != "" && d.Digest != before.Composed { + next = mwc.Want{Fp: m.FP, Composed: before.Composed} + } else if d, there := m.composition(node, before.Fp); there && d.Digest != "" && d.Digest != before.Composed { // Held back at an earlier fingerprint whose kept composition the controller composed again with today's config // (novox/hq ADR 0545): the want follows its new revision, so the node does not run that build with the config of // the moment it was made (on g14 a capacity of 0 after a rollback). Asked as any move of a record module is. at := m - at.FP = before.FP + at.FP = before.Fp running, ran := n.Haves[node][m.Name] need, wait := at.mustAsk(node, before, true, ran, running.Fp, running.Composed == "", d) if need != nil { @@ -574,13 +575,13 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st if !write { continue } - if !had || !delivery.SameWant(normal(before), normal(next)) { + if !had || !mwc.SameWant(normal(before), normal(next)) { out.Wants[node] = next } } for node, before := range wants { if !slices.Contains(m.Nodes, node) && !before.Removed { - out.Wants[node] = delivery.Want{Removed: true, Words: fmt.Sprintf("%s is no longer assigned to %s", m.Name, node)} + out.Wants[node] = mwc.Want{Removed: true, Words: fmt.Sprintf("%s is no longer assigned to %s", m.Name, node)} } } return out @@ -593,7 +594,7 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st // placement 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 delivery.Want, had bool, next delivery.Want, n Nodes, out *Outcome) (delivery.Want, bool) { +func (m Module) selfDerived(node string, before mwc.Want, had bool, next mwc.Want, n Nodes, out *Outcome) (mwc.Want, bool) { switch { case next.Removed: return next, true @@ -604,14 +605,14 @@ func (m Module) selfDerived(node string, before delivery.Want, had bool, next de return next, true } next.Self, next.Yes = false, "" - d, there := m.composition(node, next.FP) + d, there := m.composition(node, next.Fp) if !there || d.Digest == "" { - out.Rules[node] = Holding{Rule: delivery.RuleComposed, Words: fmt.Sprintf("%s on %s was composed by the node while it was placed, "+ - "and no composition of its own is named at %s now, so its want stays", m.Name, node, short(next.FP))} + out.Rules[node] = Holding{Rule: mwc.RuleComposed, Words: fmt.Sprintf("%s on %s was composed by the node while it was placed, "+ + "and no composition of its own is named at %s now, so its want stays", m.Name, node, short(next.Fp))} return next, false } at := m - at.FP = next.FP + at.FP = next.Fp running, ran := n.Haves[node][m.Name] if need, wait := at.mustAsk(node, before, had, ran, running.Fp, running.Composed == "", d); wait { if need != nil && !slices.Contains(out.Record, *need) { // the branch held back at an older fingerprint may have asked it @@ -718,7 +719,7 @@ func LiftedWords(module, fp string) string { } // normal is a want without its version, as a decoded and a composed one are compared. -func normal(w delivery.Want) delivery.Want { +func normal(w mwc.Want) mwc.Want { w.V = 0 return w } @@ -735,8 +736,8 @@ func short(fp string) string { // module the node never ran (it has no want and no have), a move judged neither, a module that is not of policy record and a want // that stays are written as any want. The bus module is no exception: it moves by the rules (ADR 0601 §1), its node backing up its // streams before it applies (§2); the assigner asks no snapshot for it. -func (m Module) mustAsk(node string, before delivery.Want, had, ran bool, running string, haveNamesNoComposition bool, d ComposedDigest) (*RecordNeed, bool) { - if (!had && !ran) || before.Removed || (before.FP == m.FP && before.Composed == d.Digest) { +func (m Module) mustAsk(node string, before mwc.Want, had, ran bool, running string, haveNamesNoComposition bool, d ComposedDigest) (*RecordNeed, bool) { + if (!had && !ran) || before.Removed || (before.Fp == m.FP && before.Composed == d.Digest) { return nil, false } // Right after the switch no node has a readable want, and a have from the old node-engine names a fingerprint and no composition @@ -773,19 +774,19 @@ func (m Module) recorded(d ComposedDigest) bool { // userListRevision is the want that names the bus module's composition at the fingerprint the node runs when that composition // differs from the one the node runs only in the bus's user list (ComposedDigest.UserListOnly), and whether there is one. A node // whose want names another fingerprint than the one it runs, or a module that does not provide the bus, has none. -func (m Module) userListRevision(node string, before delivery.Want, had bool, n Nodes) (delivery.Want, bool) { +func (m Module) userListRevision(node string, before mwc.Want, had bool, n Nodes) (mwc.Want, bool) { if !catalogue.ProvidesBus(m.Manifest) { - return delivery.Want{}, false + return mwc.Want{}, false } running, ran := n.Haves[node][m.Name] - if !ran || running.Fp == "" || (had && (before.Removed || before.FP != running.Fp)) { - return delivery.Want{}, false + if !ran || running.Fp == "" || (had && (before.Removed || before.Fp != running.Fp)) { + return mwc.Want{}, false } d, ok := m.composition(node, running.Fp) if !ok || !d.UserListOnly { - return delivery.Want{}, false + return mwc.Want{}, false } - w := delivery.Want{FP: running.Fp, Composed: d.Digest} + w := mwc.Want{Fp: running.Fp, Composed: d.Digest} if had && before.Composed == d.Digest { w.Backup, w.Words = before.Backup, before.Words } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 309819c7..4bae5fbe 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -3,17 +3,18 @@ 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" "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" + "mesh-delivery-assigner/internal/puts" "slices" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -50,8 +51,8 @@ func composedEverywhere(_, fp string) (ComposedDigest, bool) { return ComposedDigest{Digest: dig(fp), Judgement: JudgedNeither}, true } -func wantedOld() map[string]delivery.Want { - return map[string]delivery.Want{"ace": {FP: fOld, Composed: dig(fOld)}, "g14": {FP: fOld, Composed: dig(fOld)}} +func wantedOld() map[string]mwc.Want { + return map[string]mwc.Want{"ace": {Fp: fOld, Composed: dig(fOld)}, "g14": {Fp: fOld, Composed: dig(fOld)}} } // 1297/B4: with every rule holding, both nodes' wants move to the new fingerprint (the canary ace runs it healthy, so @@ -60,7 +61,7 @@ func TestEveryRuleHoldsSoTheWantsMove(t *testing.T) { m, n := ready() out := Wants(m, n, wantedOld(), nil) for _, node := range []string{"ace", "g14"} { - if out.Wants[node].FP != fNew || out.Wants[node].Rule != 0 || out.Wants[node].Composed != dig(fNew) { + if out.Wants[node].Fp != fNew || out.Wants[node].Rule != 0 || out.Wants[node].Composed != dig(fNew) { t.Errorf("%s's want = %+v, want %s", node, out.Wants[node], fNew) } } @@ -69,17 +70,17 @@ func TestEveryRuleHoldsSoTheWantsMove(t *testing.T) { // 1297/B4, one test per rule: given every rule but N holds, no want moves to F, and the rule N is named. func TestEachRuleHoldsTheWantBack(t *testing.T) { for rule, breakIt := range map[int]func(*Module, *Nodes){ - delivery.RuleBuildDone: func(m *Module, _ *Nodes) { m.Built = false }, - delivery.RuleNotHeld: func(m *Module, _ *Nodes) { m.Held = true }, - delivery.RuleCanary: func(_ *Module, n *Nodes) { + mwc.RuleBuildDone: func(m *Module, _ *Nodes) { m.Built = false }, + mwc.RuleNotHeld: func(m *Module, _ *Nodes) { m.Held = true }, + mwc.RuleCanary: func(_ *Module, n *Nodes) { n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} }, - delivery.RuleGrant: func(m *Module, _ *Nodes) { m.Granted = false }, - delivery.RuleComposed: func(m *Module, _ *Nodes) { + mwc.RuleGrant: func(m *Module, _ *Nodes) { m.Granted = false }, + mwc.RuleComposed: func(m *Module, _ *Nodes) { m.Composed = func(string, string) (ComposedDigest, bool) { return ComposedDigest{}, false } }, // 1954/D1 (ADR 0570 §4): a provision of the module on that node still asked holds its want. - delivery.RuleProvisions: func(m *Module, _ *Nodes) { + mwc.RuleProvisions: func(m *Module, _ *Nodes) { m.Provisions = func(node string) map[string]provisions.Provision { return map[string]provisions.Provision{node + ".mail.database": {Provision: "database", State: provisions.Asked}, node + ".mail.oidc": {Provision: "oidc", State: provisions.Provided}} } @@ -88,16 +89,16 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { m, n := ready() breakIt(&m, &n) out := Wants(m, n, wantedOld(), nil) - if w, ok := out.Wants["g14"]; ok && w.FP == fNew { + if w, ok := out.Wants["g14"]; ok && w.Fp == fNew { t.Errorf("rule %d broken: g14's want moved to F", rule) } if got := out.Rules["g14"].Rule; got != rule { t.Errorf("rule %d broken: g14 is held back by rule %d (%s)", rule, got, out.Rules["g14"].Words) } - if w := out.Wants["g14"]; w.FP != "" && (w.FP != fOld || w.Newest != fNew || w.Rule != rule) { + if w := out.Wants["g14"]; w.Fp != "" && (w.Fp != fOld || w.Newest != fNew || w.Rule != int64(rule)) { t.Errorf("rule %d broken: g14's want is %+v", rule, w) } - if rule == delivery.RuleProvisions && (!strings.Contains(out.Rules["g14"].Words, "g14.mail.database") || !strings.Contains(out.Rules["g14"].Words, "asked")) { + if rule == mwc.RuleProvisions && (!strings.Contains(out.Rules["g14"].Words, "g14.mail.database") || !strings.Contains(out.Rules["g14"].Words, "asked")) { t.Errorf("the provision's hold does not name its key and state: %s", out.Rules["g14"].Words) } } @@ -108,7 +109,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { }, nil} { m, n := ready() m.Provisions = all - if out := Wants(m, n, wantedOld(), nil); out.Wants["g14"].FP != fNew { + if out := Wants(m, n, wantedOld(), nil); out.Wants["g14"].Fp != fNew { t.Errorf("provided provisions held g14's want: %+v (%s)", out.Wants["g14"], out.Rules["g14"].Words) } } @@ -151,14 +152,14 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { m.Name, m.Needs, n.Reads, m.FormNeeds = c.name, c.needs, c.reads, c.forms n.Haves["ace"][c.name], n.Haves["g14"][c.name] = n.Haves["ace"]["mail"], n.Haves["g14"]["mail"] out := Wants(m, n, wantedOld(), nil) - if moved := out.Wants["g14"].FP == fNew; moved == c.held { + if moved := out.Wants["g14"].Fp == fNew; moved == c.held { t.Errorf("%s: g14's want %+v, held by %+v", c.why, out.Wants["g14"], out.Rules["g14"]) } - if r := out.Rules["g14"]; c.held && (r.Rule != delivery.RuleComposed || !strings.Contains(r.Words, c.names) || + if r := out.Rules["g14"]; c.held && (r.Rule != mwc.RuleComposed || !strings.Contains(r.Words, c.names) || !strings.Contains(r.Words, "node-engine of g14")) { t.Errorf("%s: held by %+v, want rule 7 naming the capability", c.why, r) } - if w := out.Wants["g14"]; c.held && (w.Rule != delivery.RuleComposed || w.Newest != fNew) { + if w := out.Wants["g14"]; c.held && (w.Rule != mwc.RuleComposed || w.Newest != fNew) { t.Errorf("%s: g14's want %+v, want it kept at what it runs and naming rule 7", c.why, w) } } @@ -199,12 +200,12 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } out := Wants(m, n, wantedOld(), nil) if c.holds == "" { - if out.Wants["g14"].FP != fNew { + if out.Wants["g14"].Fp != fNew { t.Errorf("%s: held %+v", c.name, out.Rules["g14"]) } continue } - if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != words { + if out.Wants["g14"].Fp == fNew || out.Rules["g14"].Rule != mwc.RuleProvisions || out.Rules["g14"].Words != words { t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"]) } } @@ -225,7 +226,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { node + ".mail.database": {Provision: "database", State: c.older, Why: "the operator's answer"}} } out := Wants(m, n, wantedOld(), nil) - if got := out.Wants["g14"].FP == fNew; got != c.moves || (!c.moves && (!strings.Contains(out.Rules["g14"].Words, "oidc") || strings.Contains(out.Rules["g14"].Words, "database"))) { + if got := out.Wants["g14"].Fp == fNew; got != c.moves || (!c.moves && (!strings.Contains(out.Rules["g14"].Words, "oidc") || strings.Contains(out.Rules["g14"].Words, "database"))) { t.Errorf("%s: want %+v, held by %q", c.name, out.Wants["g14"], out.Rules["g14"].Words) } } @@ -269,12 +270,12 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } out := Wants(m, n, wantedOld(), nil) if c.missing == "" { - if out.Wants["g14"].FP != fNew || len(m.MissingProvisions("g14")) != 0 { + if out.Wants["g14"].Fp != fNew || len(m.MissingProvisions("g14")) != 0 { t.Errorf("%s: held %+v", c.name, out.Rules["g14"]) } continue } - if out.Rules["g14"].Rule != delivery.RuleProvisions || !strings.Contains(out.Rules["g14"].Words, + if out.Rules["g14"].Rule != mwc.RuleProvisions || !strings.Contains(out.Rules["g14"].Words, "provision "+c.missing+" has no key yet") || len(m.MissingProvisions("g14")) != 1 { t.Errorf("%s: %+v", c.name, out.Rules["g14"]) } @@ -293,7 +294,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { m.Offered = func(p string) bool { return c.offered && p == c.requires } m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } out := Wants(m, n, wantedOld(), nil) - if got := out.Wants["g14"].FP == fNew; got != c.offered || (!c.offered && (out.Rules["g14"].Rule != delivery.RuleProvisions || + if got := out.Wants["g14"].Fp == fNew; got != c.offered || (!c.offered && (out.Rules["g14"].Rule != mwc.RuleProvisions || !strings.Contains(out.Rules["g14"].Words, "no module offers database yet") || !strings.Contains(out.Rules["g14"].Words, "past its bound"))) { t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"]) } @@ -304,7 +305,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { for _, c := range []struct { name, module, provision, provider string have *nodeengine.HaveByNodeModuleValue // what novox runs of the module; nil: a new assignment - before map[string]delivery.Want + before map[string]mwc.Want online bool want string }{ @@ -313,11 +314,11 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { {"link2pay, route-proxy down, new assignment", "link2pay", "route", "route-proxy", nil, nil, true, fNew}, {"photos, minio not healthy, moving its want", "photos", "s3-bucket", "minio", &nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, - map[string]delivery.Want{"novox": {FP: fOld, Composed: dig(fOld)}}, true, fNew}, + map[string]mwc.Want{"novox": {Fp: fOld, Composed: dig(fOld)}}, true, fNew}, {"novox offline, no canary, new assignment", "link2pay", "route", "route-proxy", nil, nil, false, fNew}, {"novox offline, no canary, moving its want", "photos", "s3-bucket", "minio", &nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, - map[string]delivery.Want{"novox": {FP: fOld, Composed: dig(fOld)}}, false, fOld}, + map[string]mwc.Want{"novox": {Fp: fOld, Composed: dig(fOld)}}, false, fOld}, } { m := Module{Name: c.module, FP: fNew, Built: true, Granted: true, Composed: composedEverywhere, Nodes: []string{"novox"}, Manifest: catalogue.Manifest{Module: c.module, Requires: []string{c.provision}}} @@ -332,7 +333,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { if !written { w = c.before["novox"] } - if w.FP != c.want || w.Removed { + if w.Fp != c.want || w.Removed { t.Errorf("%s: novox's want = %+v, want %s", c.name, w, short(c.want)) } if _, on := out.Neither["novox"]; on { @@ -347,8 +348,8 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { // 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. - selfAt := func(fp string) map[string]delivery.Want { - return map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}, "g14": {FP: fp, Self: true, Yes: dig(fp)}} + selfAt := func(fp string) map[string]mwc.Want { + return map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}, "g14": {Fp: fp, Self: true, Yes: dig(fp)}} } atF := func(_ *Module, n *Nodes) { n.Haves["g14"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fNew), Phase: nodeengineclient.PhaseFailed, Health: nodeengineclient.HealthOK} @@ -357,31 +358,31 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { name string set func(*Module, *Nodes) before string - want delivery.Want // the want of g14 written; zero: none written - rule int // the rule g14's holding names + want mwc.Want // the want of g14 written; zero: none written + rule int // the rule g14's holding names asked bool }{ {name: "rule 5 at F", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) n.Haves["ace"]["store"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf('s'), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthWaits} - }, want: delivery.Want{FP: fNew, Composed: dig(fNew)}}, + }, want: mwc.Want{Fp: fNew, Composed: dig(fNew)}}, {name: "rule 3 at F", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - }, want: delivery.Want{FP: fNew, Composed: dig(fNew)}, rule: delivery.RuleCanary}, + }, want: mwc.Want{Fp: fNew, Composed: dig(fNew)}, rule: mwc.RuleCanary}, {name: "rule 3 held back at an older fingerprint", before: fOld, set: func(_ *Module, n *Nodes) { n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - }, want: delivery.Want{FP: fOld, Composed: dig(fOld), Newest: fNew, Rule: delivery.RuleCanary, - Words: "the canary ace does not run bbbbbbbbbbbb healthy yet"}, rule: delivery.RuleCanary}, + }, want: mwc.Want{Fp: fOld, Composed: dig(fOld), Newest: fNew, Rule: mwc.RuleCanary, + Words: "the canary ace does not run bbbbbbbbbbbb healthy yet"}, rule: mwc.RuleCanary}, {name: "held at F with nothing to set back to", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n); m.Held = true }, - want: delivery.Want{FP: fNew, Composed: dig(fNew), Words: "mail is held at bbbbbbbbbbbb and g14 ran no other fingerprint healthy to set it back to"}, - rule: delivery.RuleNotHeld}, + want: mwc.Want{Fp: fNew, Composed: dig(fNew), Words: "mail is held at bbbbbbbbbbbb and g14 ran no other fingerprint healthy to set it back to"}, + rule: mwc.RuleNotHeld}, {name: "held data module", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) m.Held = true m.Manifest.Data = &catalogue.Data{Own: []catalogue.DataItem{{ID: "mail", Class: catalogue.ClassIrreplaceable}}} - }, want: delivery.Want{FP: fNew, Composed: dig(fNew), - Words: "mail holds data, so it is not set back from bbbbbbbbbbbb: the operator is asked whether to restore it"}, rule: delivery.RuleNotHeld}, + }, want: mwc.Want{Fp: fNew, Composed: dig(fNew), + Words: "mail holds data, so it is not set back from bbbbbbbbbbbb: the operator is asked whether to restore it"}, rule: mwc.RuleNotHeld}, {name: "a record ask waits", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) recording(m) @@ -389,7 +390,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { m.Composed = func(_, fp string) (ComposedDigest, bool) { return ComposedDigest{Digest: dig(fp), Judgement: JudgedRecreates, Against: dig(fOld)}, true } - }, rule: delivery.RuleNotHeld, asked: true}, + }, rule: mwc.RuleNotHeld, asked: true}, {name: "no composition of F", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) m.Composed = func(node, fp string) (ComposedDigest, bool) { @@ -398,7 +399,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } return composedEverywhere(node, fp) } - }, rule: delivery.RuleComposed}, + }, rule: mwc.RuleComposed}, {name: "placed", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n) m.Self = func(node string) bool { return node == "g14" } @@ -408,13 +409,13 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } return composedEverywhere(node, fp) } - }, want: delivery.Want{FP: fNew, Self: true}}, + }, want: mwc.Want{Fp: fNew, Self: true}}, } { m, n := ready() c.set(&m, &n) before := selfAt(c.before) if c.name == "placed" { - before["g14"] = delivery.Want{FP: fNew, Composed: dig(fNew)} + before["g14"] = mwc.Want{Fp: fNew, Composed: dig(fNew)} } out := Wants(m, n, before, nil) if got := out.Wants["g14"]; got != c.want { @@ -427,7 +428,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { t.Errorf("%s: record asks %+v", c.name, out.Record) } if w, ok := out.Wants["g14"]; ok { - if _, err := delivery.EncodeWant(w); err != nil { + if _, err := mwc.EncodeWant(w); err != nil { t.Errorf("%s: g14's want does not encode: %v", c.name, err) } } @@ -439,10 +440,10 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { grant := grantsstate.Grant{Accounts: []string{"ace.mail", "ace.mail.send"}} m.GrantedOn = func(node string) bool { return GrantNamesNode(grant, node, "mail") } out := Wants(m, n, wantedOld(), nil) - if w := out.Wants["ace"]; w.FP != fNew { + if w := out.Wants["ace"]; w.Fp != fNew { t.Errorf("ace, whose account the grant names, wants %+v", w) } - if got := out.Rules["g14"]; got.Rule != delivery.RuleGrant || !strings.Contains(got.Words, "on g14") { + if got := out.Rules["g14"]; got.Rule != mwc.RuleGrant || !strings.Contains(got.Words, "on g14") { t.Errorf("g14, whose account the grant does not name, is held by %+v", got) } for _, c := range []struct { @@ -472,10 +473,10 @@ func TestAnOfflineCanaryIsPassedOver(t *testing.T) { t.Fatalf("canary = %q, want g14", c) } out := Wants(m, n, wantedOld(), nil) - if out.Wants["g14"].FP != fNew { + if out.Wants["g14"].Fp != fNew { t.Errorf("the canary g14's want = %+v", out.Wants["g14"]) } - if out.Rules["ace"].Rule != delivery.RuleCanary { + if out.Rules["ace"].Rule != mwc.RuleCanary { t.Errorf("ace, behind the canary g14, is held back by %+v", out.Rules["ace"]) } } @@ -485,22 +486,22 @@ func TestAFailedHealthHoldsAndSetsBack(t *testing.T) { m, n := ready() n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "connection refused"} m.Now = time.Date(2026, 10, 25, 1, 0, 0, 0, time.UTC) - out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}, "g14": {FP: fOld, Composed: dig(fOld)}}, map[string]string{"ace": fOld}) + out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}, "g14": {Fp: fOld, Composed: dig(fOld)}}, map[string]string{"ace": fOld}) // 1906/H3: the held record keeps its cause in words: which machine (its node), which health, when, and the node's words. if out.Held == nil || out.Held.Node != "ace" || out.Held.Words != "health failed, phase running, seen at 2026-10-25T01:00:00Z: connection refused" { t.Fatalf("held = %+v", out.Held) } - if w := out.Wants["ace"]; w.FP != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule != delivery.RuleNotHeld { + if w := out.Wants["ace"]; w.Fp != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule != mwc.RuleNotHeld { t.Errorf("ace's want = %+v, want set back to the last healthy fingerprint", w) } - if w, ok := out.Wants["g14"]; ok && w.FP == fNew { + if w, ok := out.Wants["g14"]; ok && w.Fp == fNew { t.Errorf("g14 moved to a held fingerprint: %+v", w) } // 1496, ADR 0557 §1, §2: the same failure while F runs healthy on another node is that node's: F is not held, nothing is set // back, the failing node keeps its want, and the outcome names it (on ace a newly assigned module's first check held a version healthy for an hour on three nodes). n.Haves["g14"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - before := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}, "g14": {FP: fNew, Composed: dig(fNew)}} + before := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}, "g14": {Fp: fNew, Composed: dig(fNew)}} out = Wants(m, n, before, map[string]string{"ace": fOld, "g14": fOld}) if out.Held != nil || len(out.Wants) != 0 { t.Fatalf("held %+v, wants %+v: want no hold and no want changed", out.Held, out.Wants) @@ -518,12 +519,12 @@ func TestAFailedHealthHoldsAndSetsBack(t *testing.T) { n.Haves["ace"]["mail"], n.Haves["g14"]["mail"] = n.Haves["g14"]["mail"], failedHave n.Online["novox"] = true n.Haves["novox"] = map[string]nodeengine.HaveByNodeModuleValue{"mail": {Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}} - before["novox"] = delivery.Want{FP: fOld, Composed: dig(fOld)} + before["novox"] = mwc.Want{Fp: fOld, Composed: dig(fOld)} out = Wants(three, n, before, map[string]string{"ace": fOld, "g14": fOld, "novox": fOld}) if _, alone := out.NodeFailed["g14"]; !alone { t.Errorf("g14 does not fail alone: %+v", out.NodeFailed) } - if w, written := out.Wants["novox"]; out.Held != nil || len(out.NodeFailed) != 1 || (written && w.FP == fNew) || out.Rules["novox"].Rule != delivery.RuleNotHeld { + if w, written := out.Wants["novox"]; out.Held != nil || len(out.NodeFailed) != 1 || (written && w.Fp == fNew) || out.Rules["novox"].Rule != mwc.RuleNotHeld { t.Errorf("held %+v, node failed %+v, novox's want %+v (%+v): want novox kept off F", out.Held, out.NodeFailed, out.Wants["novox"], out.Rules["novox"]) } if _, written := out.Wants["g14"]; written { @@ -533,7 +534,7 @@ func TestAFailedHealthHoldsAndSetsBack(t *testing.T) { n.Haves["novox"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} delete(before, "novox") out = Wants(three, n, before, map[string]string{"ace": fOld, "g14": fOld, "novox": fOld}) - if w := out.Wants["novox"]; w.FP != fNew || len(out.Neither) != 0 { + if w := out.Wants["novox"]; w.Fp != fNew || len(out.Neither) != 0 { t.Errorf("novox running F with no want: want %+v, neither %+v; want its want at F and no neither", w, out.Neither) } n.Haves["ace"]["mail"], n.Haves["g14"]["mail"] = failedHave, n.Haves["ace"]["mail"] @@ -553,12 +554,12 @@ func TestAFailedHealthHoldsAndSetsBack(t *testing.T) { if c.novox == nodeengineclient.HealthOK { n.Haves["novox"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} } - before["novox"] = delivery.Want{FP: fNew, Composed: dig(fNew)} + before["novox"] = mwc.Want{Fp: fNew, Composed: dig(fNew)} out = Wants(three, n, before, map[string]string{"ace": fOld, "g14": fOld, "novox": fOld}) if out.Held == nil || out.Held.Node != "ace" || len(out.NodeFailed) != 0 { t.Errorf("%s: held %+v, node failed %+v: want F held at ace", c.name, out.Held, out.NodeFailed) } - if w := out.Wants["ace"]; w.FP != fOld { + if w := out.Wants["ace"]; w.Fp != fOld { t.Errorf("%s: ace's want = %+v, want set back", c.name, w) } } @@ -569,11 +570,11 @@ func TestADataModuleIsNotSetBack(t *testing.T) { m, n := ready() m.Manifest.Data = &catalogue.Data{Own: []catalogue.DataItem{{ID: "mail", Class: catalogue.ClassIrreplaceable}}} n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed} - out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fNew}}, map[string]string{"ace": fOld}) + out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fNew}}, map[string]string{"ace": fOld}) if out.Held == nil { t.Fatal("F was not held") } - if w := out.Wants["ace"]; w.FP != fNew || !strings.Contains(w.Words, "holds data") { + if w := out.Wants["ace"]; w.Fp != fNew || !strings.Contains(w.Words, "holds data") { t.Errorf("ace's want = %+v, want it left at F, saying it holds data", w) } // 1906/D3: and the node is to be asked whether to restore it @@ -586,7 +587,7 @@ func TestADataModuleIsNotSetBack(t *testing.T) { func TestAModuleWithoutDataIsNotAskedToRestore(t *testing.T) { m, n := ready() n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed} - out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fNew}}, map[string]string{"ace": fOld}) + out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fNew}}, map[string]string{"ace": fOld}) if len(out.Restore) != 0 { t.Errorf("restore asks = %v", out.Restore) } @@ -597,7 +598,7 @@ func TestAModuleWithoutDataIsNotAskedToRestore(t *testing.T) { func TestALeavingNodeIsRemovedAndNothingIsWrittenTwice(t *testing.T) { m, n := ready() m.Nodes = []string{"ace"} - out := Wants(m, n, map[string]delivery.Want{"ace": {V: 1, FP: fNew, Composed: dig(fNew)}, "g14": {FP: fOld, Composed: dig(fOld)}}, nil) + out := Wants(m, n, map[string]mwc.Want{"ace": {V: 1, Fp: fNew, Composed: dig(fNew)}, "g14": {Fp: fOld, Composed: dig(fOld)}}, nil) if w := out.Wants["g14"]; !w.Removed { t.Errorf("g14's want = %+v, want removed", w) } @@ -619,11 +620,11 @@ func TestAWantWaitsForItsCompositionAndNamesItsDigest(t *testing.T) { } out := Wants(m, n, wantedOld(), nil) for node, w := range out.Wants { - if w.FP == fNew { + if w.Fp == fNew { t.Errorf("%s's want moved to F before its composition exists: %+v", node, w) } } - if r := out.Rules["ace"]; r.Rule != delivery.RuleComposed || !strings.Contains(r.Words, "ace") { + if r := out.Rules["ace"]; r.Rule != mwc.RuleComposed || !strings.Contains(r.Words, "ace") { t.Errorf("ace is held back by %+v, want rule 7 naming the node", r) } // and a node that never had a want gets none @@ -632,10 +633,10 @@ func TestAWantWaitsForItsCompositionAndNamesItsDigest(t *testing.T) { } have["ace|"+fNew] = true out = Wants(m, n, wantedOld(), nil) - if w := out.Wants["ace"]; w.FP != fNew || w.Composed != dig(fNew) { + if w := out.Wants["ace"]; w.Fp != fNew || w.Composed != dig(fNew) { t.Errorf("ace's want = %+v, want F naming the composition's digest", w) } - if w, ok := out.Wants["g14"]; ok && w.FP == fNew { + if w, ok := out.Wants["g14"]; ok && w.Fp == fNew { t.Errorf("g14, whose composition does not exist, moved: %+v", w) } } @@ -648,10 +649,10 @@ func TestARevisionAtTheSameFingerprintIsAWantByTheSameRules(t *testing.T) { m.Composed = func(node, fp string) (ComposedDigest, bool) { return ComposedDigest{Digest: revised, Judgement: JudgedNeither}, true } - running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}, "g14": {FP: fNew, Composed: dig(fNew)}} + running := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}, "g14": {Fp: fNew, Composed: dig(fNew)}} out := Wants(m, n, running, nil) for _, node := range []string{"ace", "g14"} { - if w := out.Wants[node]; w.FP != fNew || w.Composed != revised { + if w := out.Wants[node]; w.Fp != fNew || w.Composed != revised { t.Errorf("%s's want = %+v, want F naming the revised digest", node, w) } } @@ -665,7 +666,7 @@ func TestARevisionAtTheSameFingerprintIsAWantByTheSameRules(t *testing.T) { // controller composed again with today's config (novox/hq ADR 0545: on g14 a capacity of 0 after a rollback), while // the rules still hold F; for a record module whose revision recreates a container, only after the ask. out = Wants(m, n, wantedOld(), nil) - if w := out.Wants["g14"]; w.FP != fOld || w.Composed != revised || w.Newest != fNew { + if w := out.Wants["g14"]; w.Fp != fOld || w.Composed != revised || w.Newest != fNew { t.Errorf("g14, held back at its earlier fingerprint, wants %+v; want it at that fingerprint naming the revised digest", w) } m.Composed = func(node, fp string) (ComposedDigest, bool) { @@ -685,7 +686,7 @@ func TestARevisionAtTheSameFingerprintIsAWantByTheSameRules(t *testing.T) { func TestASetBackNamesTheOlderCompositionOrStays(t *testing.T) { m, n := ready() n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "x"} - running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}} + running := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}} m.Composed = func(node, fp string) (ComposedDigest, bool) { if fp == fOld { return ComposedDigest{}, false @@ -693,18 +694,18 @@ func TestASetBackNamesTheOlderCompositionOrStays(t *testing.T) { return composedEverywhere(node, fp) } out := Wants(m, n, running, map[string]string{"ace": fOld}) - if w := out.Wants["ace"]; w.FP == fOld { + if w := out.Wants["ace"]; w.Fp == fOld { t.Errorf("set back to a fingerprint whose composition is gone: %+v", w) } m.Composed = composedEverywhere out = Wants(m, n, running, map[string]string{"ace": fOld}) - if w := out.Wants["ace"]; w.FP != fOld || w.Composed != dig(fOld) { + if w := out.Wants["ace"]; w.Fp != fOld || w.Composed != dig(fOld) { t.Errorf("set-back want = %+v", w) } } // movingTo is a module ready to move, whose composition at F is judged j against the one it runs, and whose digest is dig(F). -func movingTo(j Judgement) (Module, Nodes, map[string]delivery.Want) { +func movingTo(j Judgement) (Module, Nodes, map[string]mwc.Want) { m, n := ready() m.Composed = func(node, fp string) (ComposedDigest, bool) { d := ComposedDigest{Digest: dig(fp), Judgement: j} @@ -740,13 +741,13 @@ func TestARecordModuleWaitsForItsAskWhenTheMoveIsNotJudgedNeither(t *testing.T) t.Errorf("%s: the asks = %+v, want one for ace naming the digest and the judgement", j, out.Record) } // a new revision at the same fingerprint is asked too - same := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fOld)}} + same := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fOld)}} if out := Wants(m, n, same, nil); len(out.Record) == 0 || len(out.Wants) != 0 && out.Wants["ace"].Composed == dig(fNew) { t.Errorf("%s: a new revision at the same fingerprint: %+v", j, out) } m.Allowed = func(node, digest string) bool { return node == "ace" && digest == dig(fNew) } out = Wants(m, n, wanted, nil) - if w := out.Wants["ace"]; w.FP != fNew || w.Composed != dig(fNew) { + if w := out.Wants["ace"]; w.Fp != fNew || w.Composed != dig(fNew) { t.Errorf("%s: after the yes ace's want = %+v", j, w) } } @@ -757,25 +758,25 @@ func TestARecordModuleWaitsForItsAskWhenTheMoveIsNotJudgedNeither(t *testing.T) func TestAMoveJudgedNeitherOrNeverRunOrNotRecordedIsWrittenAtOnce(t *testing.T) { m, n, wanted := movingTo(JudgedNeither) recording(&m) - if out := Wants(m, n, wanted, nil); out.Wants["ace"].FP != fNew || len(out.Record) != 0 { + if out := Wants(m, n, wanted, nil); out.Wants["ace"].Fp != fNew || len(out.Record) != 0 { t.Errorf("neither: %+v", out) } m, n, _ = movingTo(JudgedRecreates) recording(&m) m.Nodes = []string{"ace"} // g14 runs it with no want, so its first want is asked (1945/AC19): this case is ace's alone delete(n.Haves["ace"], "mail") - if out := Wants(m, n, nil, nil); out.Wants["ace"].FP != fNew || len(out.Record) != 0 { + if out := Wants(m, n, nil, nil); out.Wants["ace"].Fp != fNew || len(out.Record) != 0 { t.Errorf("a node that never ran it: %+v", out) } // but one that runs an older fingerprint with no want yet is asked like any other (one running F is not: N1 below) m, n, _ = movingTo(JudgedCannot) recording(&m) n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - if out := Wants(m, n, nil, nil); out.Wants["ace"].FP == fNew || len(out.Record) == 0 { + if out := Wants(m, n, nil, nil); out.Wants["ace"].Fp == fNew || len(out.Record) == 0 { t.Errorf("a node that runs it with no want: %+v", out) } m, n, wanted = movingTo(JudgedRecreates) - if out := Wants(m, n, wanted, nil); out.Wants["ace"].FP != fNew || len(out.Record) != 0 { + if out := Wants(m, n, wanted, nil); out.Wants["ace"].Fp != fNew || len(out.Record) != 0 { t.Errorf("a rolled module: %+v", out) } } @@ -787,12 +788,12 @@ func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T m.Manifest.Provides = []catalogue.Offer{{Name: "mesh-bus"}} // ADR 0601 §1, §2: the bus module's want is written by the rules, as any module's, and no snapshot is asked for it: its node // backs its streams up before it applies. - if out := Wants(m, n, wantedOld(), nil); out.Wants["ace"].FP != fNew || len(out.Record) != 0 || len(out.Neither) != 0 { + if out := Wants(m, n, wantedOld(), nil); out.Wants["ace"].Fp != fNew || len(out.Record) != 0 || len(out.Neither) != 0 { t.Errorf("the bus did not move by the rules: %+v", out) } - running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fOld)}, "g14": {FP: fNew, Composed: dig(fOld)}} + running := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fOld)}, "g14": {Fp: fNew, Composed: dig(fOld)}} out := Wants(m, n, running, nil) - if w := out.Wants["ace"]; w.FP != fNew || w.Composed != dig(fNew) { + if w := out.Wants["ace"]; w.Fp != fNew || w.Composed != dig(fNew) { t.Errorf("a user-list revision of the bus: ace's want = %+v", w) } if len(out.Record) != 0 { @@ -800,23 +801,23 @@ func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T } // ADR 0546: a put owns the node's want from its asking until it ends. While it waits the rules write nothing there; once // wanted its want stands, marked put and held, and a held bus still takes its user-list revision (ADR 0539 §7). - put := delivery.Put{FP: fNew, Hold: true, Why: "the bus's user list", Composed: dig(fNew)} + put := puts.Put{FP: fNew, Hold: true, Why: "the bus's user list", Composed: dig(fNew)} for _, c := range []struct { name string step string - wanted map[string]delivery.Want - want *delivery.Want // nil: nothing written on ace + wanted map[string]mwc.Want + want *mwc.Want // nil: nothing written on ace }{ - {"waiting for the operator", delivery.PutPerson, wantedOld(), nil}, - {"wanted", delivery.PutWanted, wantedOld(), &delivery.Want{FP: fNew, Composed: dig(fNew), Put: true, Hold: true, Words: "put: the bus's user list"}}, - {"holding, written already", delivery.PutHolding, map[string]delivery.Want{"ace": PutWant(put)}, nil}, - {"released: the rules again", delivery.PutReleased, map[string]delivery.Want{"ace": PutWant(put)}, &delivery.Want{FP: fNew, Composed: dig(fNew)}}, - {"not heard: a held want at an older fingerprint stands", "", map[string]delivery.Want{"ace": {FP: fOld, Composed: dig(fOld), Put: true, Hold: true, Words: "put: back"}}, nil}, - {"not heard: a held want the rules would rewrite stands", "", map[string]delivery.Want{"ace": PutWant(put)}, nil}, + {"waiting for the operator", puts.Person, wantedOld(), nil}, + {"wanted", puts.Wanted, wantedOld(), &mwc.Want{Fp: fNew, Composed: dig(fNew), Put: true, Hold: true, Words: "put: the bus's user list"}}, + {"holding, written already", puts.Holding, map[string]mwc.Want{"ace": PutWant(put)}, nil}, + {"released: the rules again", puts.Released, map[string]mwc.Want{"ace": PutWant(put)}, &mwc.Want{Fp: fNew, Composed: dig(fNew)}}, + {"not heard: a held want at an older fingerprint stands", "", map[string]mwc.Want{"ace": {Fp: fOld, Composed: dig(fOld), Put: true, Hold: true, Words: "put: back"}}, nil}, + {"not heard: a held want the rules would rewrite stands", "", map[string]mwc.Want{"ace": PutWant(put)}, nil}, } { p := put p.Step = c.step - m.Puts = map[string]delivery.Put{"ace": p} + m.Puts = map[string]puts.Put{"ace": p} if c.step == "" { m.Puts = nil } @@ -825,7 +826,7 @@ func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T switch { case c.want == nil && wrote: t.Errorf("put %s: the rules wrote ace's want %+v", c.name, got) - case c.want != nil && (!wrote || !delivery.SameWant(normal(got), *c.want)): + case c.want != nil && (!wrote || !mwc.SameWant(normal(got), *c.want)): t.Errorf("put %s: ace's want = %+v (written %v), want %+v", c.name, got, wrote, *c.want) } } @@ -835,9 +836,9 @@ func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T } n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fNew), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} held := put - held.Step = delivery.PutHolding - m.Puts = map[string]delivery.Put{"ace": held} - if w := Wants(m, n, map[string]delivery.Want{"ace": PutWant(held)}, nil).Wants["ace"]; w.Composed != userList || !w.Put || !w.Hold { + held.Step = puts.Holding + m.Puts = map[string]puts.Put{"ace": held} + if w := Wants(m, n, map[string]mwc.Want{"ace": PutWant(held)}, nil).Wants["ace"]; w.Composed != userList || !w.Put || !w.Hold { t.Errorf("a held bus did not take its user-list revision as a put: %+v", w) } m.Puts = nil @@ -849,7 +850,7 @@ func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T func stepsOfAPut(t *testing.T) { t.Helper() now := time.Date(2026, 10, 25, 22, 0, 0, 0, time.UTC) - base := delivery.Put{FP: fNew, Hold: true, Why: "the bus's user list", At: now}.Said(now, delivery.PutAsked, "asked") + base := puts.Put{FP: fNew, Hold: true, Why: "the bus's user list", At: now}.Said(now, puts.Asked, "asked") d := ComposedDigest{Digest: dig(fNew), Judgement: JudgedCannot, Words: "the have names no composition"} running := nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} bus := func() PutWorld { @@ -864,38 +865,38 @@ func stepsOfAPut(t *testing.T) { ask, snap bool warranted bool }{ - {"not assigned", func() PutWorld { w := bus(); w.Assigned = false; return w }, delivery.PutRefused, "not assigned", false, false, false}, - {"no build yet of the newest", func() PutWorld { w := bus(); w.Build = ""; return w }, delivery.PutBuild, "builds by itself", false, false, false}, - {"no build of an older one", func() PutWorld { w := bus(); w.Build, w.Newest = "", fpOf('c'); return w }, delivery.PutRefused, "holds no build", false, false, false}, - {"a failed build", func() PutWorld { w := bus(); w.Build, w.BuildWords = builds.BuildFailed, "go vet"; return w }, delivery.PutRefused, "go vet", false, false, false}, - {"no composition yet: waits", func() PutWorld { w := bus(); w.Composed = nil; return w }, delivery.PutCompose, "no composition of nats for novox", false, false, false}, - {"composed, grant not naming the node", func() PutWorld { w := bus(); w.Granted = false; return w }, delivery.PutCompose, "does not name novox", false, false, false}, + {"not assigned", func() PutWorld { w := bus(); w.Assigned = false; return w }, puts.Refused, "not assigned", false, false, false}, + {"no build yet of the newest", func() PutWorld { w := bus(); w.Build = ""; return w }, puts.Build, "builds by itself", false, false, false}, + {"no build of an older one", func() PutWorld { w := bus(); w.Build, w.Newest = "", fpOf('c'); return w }, puts.Refused, "holds no build", false, false, false}, + {"a failed build", func() PutWorld { w := bus(); w.Build, w.BuildWords = builds.BuildFailed, "go vet"; return w }, puts.Refused, "go vet", false, false, false}, + {"no composition yet: waits", func() PutWorld { w := bus(); w.Composed = nil; return w }, puts.Compose, "no composition of nats for novox", false, false, false}, + {"composed, grant not naming the node", func() PutWorld { w := bus(); w.Granted = false; return w }, puts.Compose, "does not name novox", false, false, false}, {"none within the bound: refused", func() PutWorld { w := bus() w.Composed = nil - w.Put = w.Put.Said(now.Add(-ComposeWithin-time.Second), delivery.PutCompose, "waits") + w.Put = w.Put.Said(now.Add(-ComposeWithin-time.Second), puts.Compose, "waits") return w - }, delivery.PutRefused, "mesh-hand-compose", false, false, false}, - {"the bus asks the operator", bus, delivery.PutPerson, "waits for the operator's yes", true, false, false}, - {"the operator said no", func() PutWorld { w := bus(); w.Answer = OptionNo; return w }, delivery.PutRefused, "answered no", false, false, false}, - {"the ask expired unanswered", func() PutWorld { w := bus(); w.AskExpired = true; return w }, delivery.PutRefused, "expired unanswered", false, false, false}, - {"yes: the snapshot is asked", func() PutWorld { w := bus(); w.Answer = OptionYes; return w }, delivery.PutSnapshot, "asked novox's backup holder", false, true, false}, - {"the snapshot is taken", func() PutWorld { w := bus(); w.Answer, w.Snapshotting = OptionYes, true; return w }, delivery.PutSnapshot, "waits for novox's backup holder", false, false, false}, - {"the snapshot failed", func() PutWorld { w := bus(); w.Answer, w.SnapshotFailed = OptionYes, "no restic"; return w }, delivery.PutRefused, "no restic", false, false, false}, + }, puts.Refused, "mesh-hand-compose", false, false, false}, + {"the bus asks the operator", bus, puts.Person, "waits for the operator's yes", true, false, false}, + {"the operator said no", func() PutWorld { w := bus(); w.Answer = OptionNo; return w }, puts.Refused, "answered no", false, false, false}, + {"the ask expired unanswered", func() PutWorld { w := bus(); w.AskExpired = true; return w }, puts.Refused, "expired unanswered", false, false, false}, + {"yes: the snapshot is asked", func() PutWorld { w := bus(); w.Answer = OptionYes; return w }, puts.Snapshot, "asked novox's backup holder", false, true, false}, + {"the snapshot is taken", func() PutWorld { w := bus(); w.Answer, w.Snapshotting = OptionYes, true; return w }, puts.Snapshot, "waits for novox's backup holder", false, false, false}, + {"the snapshot failed", func() PutWorld { w := bus(); w.Answer, w.SnapshotFailed = OptionYes, "no restic"; return w }, puts.Refused, "no restic", false, false, false}, {"snapshotted: the want, warranted", func() PutWorld { w := bus() w.Answer, w.Snapshot = OptionYes, "novox's restore point" w.Put.Ask, _ = asks.RecordAskKey("novox", "nats", d.Digest) return w - }, delivery.PutWanted, "held until released; the streams were snapshotted: novox's restore point", false, false, true}, + }, puts.Wanted, "held until released; the streams were snapshotted: novox's restore point", false, false, true}, {"a user-list revision of the bus needs nobody", func() PutWorld { w := bus() ul := d ul.UserListOnly = true w.Composed, w.Have = &ul, &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fOld), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} return w - }, delivery.PutWanted, "held until released", false, false, false}, - {"a module not recorded needs nobody", func() PutWorld { w := bus(); w.Bus = false; return w }, delivery.PutWanted, "names", false, false, false}, + }, puts.Wanted, "held until released", false, false, false}, + {"a module not recorded needs nobody", func() PutWorld { w := bus(); w.Bus = false; return w }, puts.Wanted, "names", false, false, false}, {"a recorded module judged to recreate asks", func() PutWorld { w := bus() w.Bus, w.Recorded = false, true @@ -903,23 +904,23 @@ func stepsOfAPut(t *testing.T) { r.Judgement = JudgedRecreates w.Composed = &r return w - }, delivery.PutPerson, "waits for the operator's yes", true, false, false}, + }, puts.Person, "waits for the operator's yes", true, false, false}, {"wanted, the node runs it: holding", func() PutWorld { w := bus() - w.Put = w.Put.Said(now, delivery.PutWanted, "wanted") + w.Put = w.Put.Said(now, puts.Wanted, "wanted") w.Put.Composed = d.Digest w.Have = &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: d.Digest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} return w - }, delivery.PutHolding, "held there until released", false, false, false}, + }, puts.Holding, "held there until released", false, false, false}, {"wanted without hold: done", func() PutWorld { w := bus() w.Put.Hold = false - w.Put = w.Put.Said(now, delivery.PutWanted, "wanted") + w.Put = w.Put.Said(now, puts.Wanted, "wanted") w.Put.Composed = d.Digest w.Have = &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: d.Digest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} return w - }, delivery.PutDone, "the rules write its want again", false, false, false}, - {"released: nothing more", func() PutWorld { w := bus(); w.Put = w.Put.Said(now, delivery.PutReleased, "released"); return w }, delivery.PutReleased, "released", false, false, false}, + }, puts.Done, "the rules write its want again", false, false, false}, + {"released: nothing more", func() PutWorld { w := bus(); w.Put = w.Put.Said(now, puts.Released, "released"); return w }, puts.Released, "released", false, false, false}, } for _, c := range cases { next := StepPut(c.world()) @@ -939,14 +940,14 @@ func stepsOfAPut(t *testing.T) { // 1906/D4 (ADR 0539 §16): the answer to a failed backup stays in the want while its target stays. func TestTheBackupAnswerStaysInTheWantWhileItsTargetStays(t *testing.T) { m, n := ready() - running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew), Backup: delivery.BackupSkip, Words: BackupAnswerWords("skip", 0)}} + running := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew), Backup: mwc.BackupSkip, Words: BackupAnswerWords("skip", 0)}} if w, ok := Wants(m, n, running, nil).Wants["ace"]; ok { t.Errorf("the answer was erased: %+v", w) } // A placed node composes its own: its want names the build alone, and the answer stays in it all the same (the digest its // have names is not in the want, so a want kept for its digest would erase the answer at every pass). m.Self = func(string) bool { return true } - placed := map[string]delivery.Want{"ace": {FP: fNew, Self: true, Backup: delivery.BackupSkip, Words: BackupAnswerWords("skip", 0)}} + placed := map[string]mwc.Want{"ace": {Fp: fNew, Self: true, Backup: mwc.BackupSkip, Words: BackupAnswerWords("skip", 0)}} if w, ok := Wants(m, n, placed, nil).Wants["ace"]; ok { t.Errorf("the answer in a placed node's want was erased: %+v", w) } @@ -958,11 +959,11 @@ func TestAReleasedHoldIsNotHeldAgain(t *testing.T) { m, n := ready() n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed, Detail: "x"} m.Released = true - out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fOld, Composed: dig(fOld), Newest: fNew, Rule: delivery.RuleNotHeld}}, map[string]string{"ace": fOld}) + out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fOld, Composed: dig(fOld), Newest: fNew, Rule: mwc.RuleNotHeld}}, map[string]string{"ace": fOld}) if out.Held != nil { t.Errorf("a released fingerprint was held again: %+v", out.Held) } - if w := out.Wants["ace"]; w.FP != fNew || w.Composed != dig(fNew) || w.Rule != 0 { + if w := out.Wants["ace"]; w.Fp != fNew || w.Composed != dig(fNew) || w.Rule != 0 { t.Errorf("ace's want = %+v, want the held fingerprint written again", w) } } @@ -973,13 +974,13 @@ func TestARestoreAnswerSetsADataModuleBack(t *testing.T) { m, n := ready() m.Manifest.Data = &catalogue.Data{Own: []catalogue.DataItem{{ID: "mail", Class: catalogue.ClassIrreplaceable}}} n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed} - running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}} + running := map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}} m.Restored = func(node string) bool { return false } - if w, ok := Wants(m, n, running, map[string]string{"ace": fOld}).Wants["ace"]; ok && w.FP == fOld { + if w, ok := Wants(m, n, running, map[string]string{"ace": fOld}).Wants["ace"]; ok && w.Fp == fOld { t.Errorf("set back without the operator's restore: %+v", w) } m.Restored = func(node string) bool { return node == "ace" } - if w := Wants(m, n, running, map[string]string{"ace": fOld}).Wants["ace"]; w.FP != fOld || w.Composed != dig(fOld) { + if w := Wants(m, n, running, map[string]string{"ace": fOld}).Wants["ace"]; w.Fp != fOld || w.Composed != dig(fOld) { t.Errorf("after restore ace's want = %+v", w) } } @@ -997,12 +998,12 @@ func TestTheRecordPolicyIsTheOneTheControllerHolds(t *testing.T) { } } with(catalogue.PolicyRecord, "person") // a roll module the operator set to record - if out := Wants(m, n, wanted, nil); len(out.Record) == 0 || out.Wants["ace"].FP == fNew { + if out := Wants(m, n, wanted, nil); len(out.Record) == 0 || out.Wants["ace"].Fp == fNew { t.Errorf("the operator's record policy was not honoured: %+v", out) } recording(&m) // the manifest says record, the operator chose roll with("roll", "person") - if out := Wants(m, n, wanted, nil); len(out.Record) != 0 || out.Wants["ace"].FP != fNew { + if out := Wants(m, n, wanted, nil); len(out.Record) != 0 || out.Wants["ace"].Fp != fNew { t.Errorf("the operator's roll policy was not honoured: %+v", out) } with("", "") // made by hand: the manifest decides @@ -1012,7 +1013,7 @@ func TestTheRecordPolicyIsTheOneTheControllerHolds(t *testing.T) { // 1953/D4 (ADR 0565 §7): a person's choice at the assigner is over the controller's beside the composition. with(catalogue.PolicyRecord, "person") m.Chosen = &catalogue.PersonPolicy{V: catalogue.PersonPolicyVersion, Module: m.Name, Policy: catalogue.PolicyRoll, Why: "w", By: "op"} - if out := Wants(m, n, wanted, nil); len(out.Record) != 0 || out.Wants["ace"].FP != fNew { + if out := Wants(m, n, wanted, nil); len(out.Record) != 0 || out.Wants["ace"].Fp != fNew { t.Errorf("a person's roll at the assigner was not honoured over the controller's record: %+v", out) } } @@ -1023,11 +1024,11 @@ func TestTheBusWithNoWantIsComparedWithWhatItRuns(t *testing.T) { m, n, _ := movingTo(JudgedNeither) m.Manifest.Provides = []catalogue.Offer{{Name: "mesh-bus"}} n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - if w, ok := Wants(m, n, nil, nil).Wants["ace"]; !ok || w.FP != fNew { + if w, ok := Wants(m, n, nil, nil).Wants["ace"]; !ok || w.Fp != fNew { t.Errorf("the bus runs F with no want and was not given one: %+v %v", w, ok) } n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - if w, ok := Wants(m, n, nil, nil).Wants["ace"]; !ok || w.FP != fNew { + if w, ok := Wants(m, n, nil, nil).Wants["ace"]; !ok || w.Fp != fNew { t.Errorf("the bus running an older fingerprint with no want was not given its want by the rules: %+v %v", w, ok) } } @@ -1049,12 +1050,12 @@ func TestAUserListOnlyRevisionOfTheBusIsWrittenAtOnceBeforeTheRecordHold(t *test return d, true } n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fOld, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - for name, wanted := range map[string]map[string]delivery.Want{ - "a want at the fingerprint it runs": {"ace": {FP: fOld, Composed: dig(fOld)}}, + for name, wanted := range map[string]map[string]mwc.Want{ + "a want at the fingerprint it runs": {"ace": {Fp: fOld, Composed: dig(fOld)}}, "no readable want": nil, } { out := Wants(m, n, wanted, nil) - if w := out.Wants["ace"]; w.FP != fOld || w.Composed != userList { + if w := out.Wants["ace"]; w.Fp != fOld || w.Composed != userList { t.Errorf("%s: ace's want = %+v, want the running fingerprint naming the user-list revision", name, w) } if len(out.Record) != 0 { @@ -1063,7 +1064,7 @@ func TestAUserListOnlyRevisionOfTheBusIsWrittenAtOnceBeforeTheRecordHold(t *test } // the same flag on a module that is not the bus changes nothing m.Manifest.Provides = nil - if out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fOld, Composed: dig(fOld)}}, nil); out.Wants["ace"].Composed == userList { + if out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fOld, Composed: dig(fOld)}}, nil); out.Wants["ace"].Composed == userList { t.Errorf("a module that is not the bus was written at once: %+v", out.Wants["ace"]) } } @@ -1087,7 +1088,7 @@ func TestAHaveFromBeforeTheSwitchGetsItsWantWithNoRecordHold(t *testing.T) { m.Composed = unchanged n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} // no composition named out := Wants(m, n, nil, nil) - if w := out.Wants["ace"]; w.FP != fNew || w.Composed != dig(fNew) { + if w := out.Wants["ace"]; w.Fp != fNew || w.Composed != dig(fNew) { t.Errorf("%s: ace's want = %+v, want F naming its composition", name, w) } if len(out.Record) != 0 { @@ -1105,7 +1106,7 @@ func TestAHaveFromBeforeTheSwitchGetsItsWantWithNoRecordHold(t *testing.T) { n2.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} out := Wants(m2, n2, nil, nil) if j == JudgedNeither { - if out.Wants["ace"].FP != fNew || len(out.Record) != 0 { + if out.Wants["ace"].Fp != fNew || len(out.Record) != 0 { t.Errorf("%s, %s: %+v", name, what, out) } continue @@ -1119,7 +1120,7 @@ func TestAHaveFromBeforeTheSwitchGetsItsWantWithNoRecordHold(t *testing.T) { return ComposedDigest{Digest: dig(fp), Judgement: JudgedRecreates, Against: dig(fOld), Words: "judged"}, true } n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fOld), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} - if out := Wants(m, n, nil, nil); out.Wants["ace"].FP == fNew && len(out.Record) == 0 { + if out := Wants(m, n, nil, nil); out.Wants["ace"].Fp == fNew && len(out.Record) == 0 { t.Errorf("%s: a have naming a composition skipped the hold: %+v", name, out) } // a new fingerprint still asks (the bus is barred instead) @@ -1138,7 +1139,7 @@ func TestAHaveFromBeforeTheSwitchGetsItsWantWithNoRecordHold(t *testing.T) { func TestAHoldOfAFailureWithoutWordsSaysSo(t *testing.T) { m, n := ready() n.Haves["ace"]["mail"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed} - out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}}, map[string]string{"ace": fOld}) + out := Wants(m, n, map[string]mwc.Want{"ace": {Fp: fNew, Composed: dig(fNew)}}, map[string]string{"ace": fOld}) if out.Held == nil || out.Held.Words != "health failed, phase running: the node gave no words" { t.Fatalf("held = %+v", out.Held) } @@ -1160,7 +1161,7 @@ func TestAHoldIsLiftedOnceEveryNodeRunsItHealthy(t *testing.T) { t.Fatalf("lifted = %v, held = %+v: want the hold lifted", out.Lifted, out.Held) } for _, node := range []string{"ace", "g14"} { - if w := out.Wants[node]; w.FP != fNew || w.Rule != 0 { + if w := out.Wants[node]; w.Fp != fNew || w.Rule != 0 { t.Errorf("%s's want = %+v, want it moved to the lifted fingerprint", node, w) } } @@ -1217,10 +1218,10 @@ func TestANodeRunningAModuleWithNoWantGetsItsFirstWantAtWhatItRuns(t *testing.T) m, n := x11() out := Wants(m, n, nil, nil) // 1960: the providers rule holds no want: the canary g14 gets F, its provider's health unknown said - if w := out.Wants["g14"]; w.FP != fNew || out.Unreached["g14"] == "" { + if w := out.Wants["g14"]; w.Fp != fNew || out.Unreached["g14"] == "" { t.Errorf("g14's want = %+v (unreached %q), want F, the provider said", w, out.Unreached["g14"]) } - if w := out.Wants["shanks"]; w.FP != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule != delivery.RuleCanary { + if w := out.Wants["shanks"]; w.Fp != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule != mwc.RuleCanary { t.Errorf("shanks's want = %+v, want its first want at the fingerprint it runs, held back from F by the canary", w) } if len(out.Neither) != 0 { @@ -1230,14 +1231,14 @@ func TestANodeRunningAModuleWithNoWantGetsItsFirstWantAtWhatItRuns(t *testing.T) m, n = x11() n.Haves["g14"]["xorg"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf('x'), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} n.Haves["shanks"]["xorg"] = n.Haves["g14"]["xorg"] - if w := Wants(m, n, nil, nil).Wants["shanks"]; w.FP != fOld || w.Rule != delivery.RuleCanary { + if w := Wants(m, n, nil, nil).Wants["shanks"]; w.Fp != fOld || w.Rule != mwc.RuleCanary { t.Errorf("shanks held back by the canary with no want: %+v, want at fOld by rule 3", w) } // a node already running F gets its want at F; a removed want is no want either m, n = x11() n.Haves["g14"]["screen-lock"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthUnknown} - out = Wants(m, n, map[string]delivery.Want{"g14": {Removed: true}}, nil) - if w := out.Wants["g14"]; w.FP != fNew || w.Composed != dig(fNew) || w.Removed { + out = Wants(m, n, map[string]mwc.Want{"g14": {Removed: true}}, nil) + if w := out.Wants["g14"]; w.Fp != fNew || w.Composed != dig(fNew) || w.Removed { t.Errorf("g14 running F with a removed want = %+v, want F", w) } } @@ -1264,13 +1265,13 @@ func TestNoFirstWantWithoutTheCompositionAndGrantOfWhatItRuns(t *testing.T) { if w, ok := out.Wants["shanks"]; ok { t.Errorf("%s: shanks got a want: %+v", what, w) } - if h, ok := out.Neither["shanks"]; !ok || h.Rule != delivery.RuleCanary { + if h, ok := out.Neither["shanks"]; !ok || h.Rule != mwc.RuleCanary { t.Errorf("%s: shanks on neither list = %+v (named %v), want rule 3", what, h, ok) } continue } // 1960: no want can name what it runs, so its first want names F rather than none: never on neither list - if w := out.Wants["shanks"]; w.FP != fNew { + if w := out.Wants["shanks"]; w.Fp != fNew { t.Errorf("%s: shanks's want = %+v, want F", what, w) } if h, ok := out.Neither["shanks"]; ok { @@ -1278,16 +1279,16 @@ func TestNoFirstWantWithoutTheCompositionAndGrantOfWhatItRuns(t *testing.T) { } } m, n := x11() - before := map[string]delivery.Want{"g14": {FP: fOld, Composed: dig(fOld)}, "shanks": {FP: fOld, Composed: dig(fOld)}} + before := map[string]mwc.Want{"g14": {Fp: fOld, Composed: dig(fOld)}, "shanks": {Fp: fOld, Composed: dig(fOld)}} out := Wants(m, n, before, nil) if len(out.Neither) != 0 { t.Errorf("nodes holding a want named on neither list: %+v", out.Neither) } // the canary g14 moves to F (its provider's health no rule); shanks waits for it - if w := out.Wants["shanks"]; w.FP != "" && w.FP != fOld { + if w := out.Wants["shanks"]; w.Fp != "" && w.Fp != fOld { t.Errorf("shanks's want moved from its want past the canary: %+v", w) } - if w := out.Wants["g14"]; w.FP != fNew { + if w := out.Wants["g14"]; w.Fp != fNew { t.Errorf("the canary g14's want = %+v, want F", w) } } @@ -1300,31 +1301,31 @@ func TestANewAssignmentWaitingIsNotOnNeitherList(t *testing.T) { delete(n.Haves["shanks"], "screen-lock") out := Wants(m, n, nil, nil) // 1960: the canary orders moves, never a first want: the new assignment is wanted at F - if w := out.Wants["shanks"]; w.FP != fNew { + if w := out.Wants["shanks"]; w.Fp != fNew { t.Errorf("a new assignment while the canary does not run F: %+v, want F", w) } if _, ok := out.Neither["shanks"]; ok { t.Errorf("a new assignment is named on neither list: %+v", out.Neither) } m.Built = false - if out := Wants(m, n, nil, nil); len(out.Neither) != 1 || out.Neither["g14"].Rule != delivery.RuleBuildDone { + if out := Wants(m, n, nil, nil); len(out.Neither) != 1 || out.Neither["g14"].Rule != mwc.RuleBuildDone { t.Errorf("with no build: neither = %+v, want g14 alone (it runs the module), by rule 1", out.Neither) } // 1945 (the assigner holds a first want back silently): with no build, a node whose want was removed and that does not run // the module has the reason written into its removed want, once; a node with no key gets no want written, and a node that // runs the module is on neither list, its removed want left as it is. - gone := delivery.Want{Removed: true, Words: "screen-lock is no longer assigned to shanks"} + gone := mwc.Want{Removed: true, Words: "screen-lock is no longer assigned to shanks"} for _, c := range []struct { name string - before map[string]delivery.Want - want map[string]delivery.Want + before map[string]mwc.Want + want map[string]mwc.Want }{ - {"no key: nothing written", nil, map[string]delivery.Want{}}, - {"a removed want: it says why", map[string]delivery.Want{"shanks": gone}, - map[string]delivery.Want{"shanks": {Removed: true, Words: "no want: rule 1: the build of screen-lock at " + short(m.FP) + " is not done"}}}, - {"a removed want that says why already: nothing written", map[string]delivery.Want{"shanks": {Removed: true, Words: "no want: rule 1: the build of screen-lock at " + short(m.FP) + " is not done"}}, - map[string]delivery.Want{}}, - {"a node that runs it: on neither list, its removed want left", map[string]delivery.Want{"g14": gone}, map[string]delivery.Want{}}, + {"no key: nothing written", nil, map[string]mwc.Want{}}, + {"a removed want: it says why", map[string]mwc.Want{"shanks": gone}, + map[string]mwc.Want{"shanks": {Removed: true, Words: "no want: rule 1: the build of screen-lock at " + short(m.FP) + " is not done"}}}, + {"a removed want that says why already: nothing written", map[string]mwc.Want{"shanks": {Removed: true, Words: "no want: rule 1: the build of screen-lock at " + short(m.FP) + " is not done"}}, + map[string]mwc.Want{}}, + {"a node that runs it: on neither list, its removed want left", map[string]mwc.Want{"g14": gone}, map[string]mwc.Want{}}, } { out := Wants(m, n, c.before, nil) if len(out.Wants) != len(c.want) { @@ -1332,7 +1333,7 @@ func TestANewAssignmentWaitingIsNotOnNeitherList(t *testing.T) { continue } for node, w := range c.want { - if !delivery.SameWant(normal(out.Wants[node]), w) { + if !mwc.SameWant(normal(out.Wants[node]), w) { t.Errorf("%s: want of %s = %+v, want %+v", c.name, node, out.Wants[node], w) } } @@ -1362,7 +1363,7 @@ func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) { oldForm(n, "shanks") n.Haves["g14"]["screen-lock"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} out := Wants(m, n, nil, nil) - if w := out.Wants["shanks"]; w.FP != fNew || w.Composed != dig(fNew) || w.Newest != "" || w.Rule != 0 { + if w := out.Wants["shanks"]; w.Fp != fNew || w.Composed != dig(fNew) || w.Newest != "" || w.Rule != 0 { t.Errorf("canary healthy on F: shanks's want = %+v, want F", w) } if _, ok := out.Neither["shanks"]; ok { @@ -1377,7 +1378,7 @@ func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) { if w, ok := out.Wants["shanks"]; ok { t.Errorf("canary not healthy on F: shanks got a want: %+v", w) } - if h, ok := out.Neither["shanks"]; !ok || h.Rule != delivery.RuleCanary { + if h, ok := out.Neither["shanks"]; !ok || h.Rule != mwc.RuleCanary { t.Errorf("canary not healthy on F: shanks on neither list = %+v (named %v), want rule 3", h, ok) } // 1960: the canary healthy on F but the providers not healthy on shanks: F all the same, the provider down said @@ -1385,7 +1386,7 @@ func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) { oldForm(n, "shanks") n.Haves["g14"]["screen-lock"] = nodeengine.HaveByNodeModuleValue{Fp: fNew, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} out = Wants(m, n, nil, nil) - if w := out.Wants["shanks"]; w.FP != fNew || out.Unreached["shanks"] == "" { + if w := out.Wants["shanks"]; w.Fp != fNew || out.Unreached["shanks"] == "" { t.Errorf("providers not healthy: shanks's want = %+v (unreached %q), want F, the provider said", w, out.Unreached["shanks"]) } if _, on := out.Neither["shanks"]; on { @@ -1404,7 +1405,7 @@ func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) { m, n = x11() providersHealthy(n) oldForm(n, "g14") - if w := Wants(m, n, nil, nil).Wants["g14"]; w.FP != fNew || w.Composed != dig(fNew) { + if w := Wants(m, n, nil, nil).Wants["g14"]; w.Fp != fNew || w.Composed != dig(fNew) { t.Errorf("old-form canary: g14's want = %+v, want F", w) } } @@ -1418,7 +1419,7 @@ func TestAFailedHealthWhileTheNodeMovesHoldsNothing(t *testing.T) { Manifest: catalogue.Manifest{Module: "mesh-controller"}} n := Nodes{Online: map[string]bool{"novox": true}, Haves: map[string]map[string]nodeengine.HaveByNodeModuleValue{ "novox": {"mesh-controller": {Fp: fNew, Phase: phase, Health: nodeengineclient.HealthFailed}}}} - before := map[string]delivery.Want{"novox": {FP: fNew, Composed: dig(fNew)}} + before := map[string]mwc.Want{"novox": {Fp: fNew, Composed: dig(fNew)}} if out := Wants(m, n, before, map[string]string{"novox": fOld}); out.Held != nil { t.Errorf("phase %s, health failed: held %+v, want no hold while the node moves", phase, out.Held) } @@ -1428,7 +1429,7 @@ func TestAFailedHealthWhileTheNodeMovesHoldsNothing(t *testing.T) { Manifest: catalogue.Manifest{Module: "mesh-controller"}} n := Nodes{Online: map[string]bool{"novox": true}, Haves: map[string]map[string]nodeengine.HaveByNodeModuleValue{ "novox": {"mesh-controller": {Fp: fNew, Phase: phase, Health: nodeengineclient.HealthFailed}}}} - before := map[string]delivery.Want{"novox": {FP: fNew, Composed: dig(fNew)}} + before := map[string]mwc.Want{"novox": {Fp: fNew, Composed: dig(fNew)}} if out := Wants(m, n, before, map[string]string{"novox": fOld}); out.Held == nil || out.Held.Node != "novox" { t.Errorf("phase %s, health failed: held %+v, want a hold on novox", phase, out.Held) } @@ -1477,7 +1478,7 @@ func TestAFirstWantAfterARemovedWantGoesThroughTheRecordAsk(t *testing.T) { m.Composed = func(_, fp string) (ComposedDigest, bool) { return ComposedDigest{Digest: dig(fp), Judgement: JudgedRecreates, Policy: catalogue.PolicyRecord}, true } - out := Wants(m, n, map[string]delivery.Want{"shanks": {Removed: true}}, nil) + out := Wants(m, n, map[string]mwc.Want{"shanks": {Removed: true}}, nil) if w, ok := out.Wants["shanks"]; ok { t.Errorf("shanks's first want after a removed want was written with no record ask: %+v", w) } @@ -1511,7 +1512,7 @@ func TestAHeldModuleWithNoWantIsNotOnNeitherList(t *testing.T) { if len(out.Neither) != 0 { t.Errorf("%s: a held module is on neither list: %+v", what, out.Neither) } - if out.Rules["shanks"].Rule != delivery.RuleNotHeld { + if out.Rules["shanks"].Rule != mwc.RuleNotHeld { t.Errorf("%s: shanks is held back by %+v, want rule 2", what, out.Rules["shanks"]) } } diff --git a/modules/mesh-delivery-assigner/internal/puts/puts.go b/modules/mesh-delivery-assigner/internal/puts/puts.go new file mode 100644 index 00000000..2ba7ee20 --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/puts/puts.go @@ -0,0 +1,156 @@ +// Package puts is the assigner's put: one version of a module put on one node by its tool `assigner_put` (novox/hq +// ADR 0546), kept under `.` in its state puts. It is the assigner's own value, which no other module reads +// (the earlier form lived in mesh-sdk's go/delivery; ADR 0634 §5), so the check and the codec live here. +package puts + +import ( + "encoding/json" + "fmt" + "regexp" + "time" + + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// Each put is the key `.` of the assigner's state puts, which holds the version asked, the hold, why, and each +// step the assigner took or waits on, so a reader watches the key to follow it. The want the put writes carries Put (and +// Hold), so the assigner's rules pass over that key while the put holds it. + +// History is the revisions the puts state keeps per key: each step of a put is one. +const History = 20 + +// A put's steps, in the order they are taken (ADR 0546 clauses 3 to 9). A put stands at the last step written. +const ( + // Asked is a put written by the tool, not taken up by a pass yet. + Asked = "asked" + // Build waits for the builder's build at the fingerprint. + Build = "build" + // Compose waits for the composition (and grant) at the fingerprint. + Compose = "compose" + // Person waits for the operator's answer to the ask about the move. + Person = "person" + // Snapshot waits for the bus's streams to be snapshotted after the operator's yes. + Snapshot = "snapshot" + // Wanted is the want written: the node's have has not reached it yet. + Wanted = "wanted" + // Holding is a held put whose have runs it healthy: the want stays until released. + Holding = "holding" + // Done is a put without hold whose have ran it healthy: the rules write the want again. + Done = "done" + // Released is a put released by the tool `assigner_release`. + Released = "released" + // Refused is a put that cannot go on: a build that failed, an answer no, a snapshot not taken. Its words say why. + Refused = "refused" +) + +// Ended says a put at this step is over: nothing more is done for it and the rules own the want. +func Ended(step string) bool { + return step == Done || step == Released || step == Refused +} + +// Step is one step a put took: when, which, and what was done or waits, in a sentence. +type Step struct { + At time.Time `json:"at"` + Step string `json:"step"` + Words string `json:"words"` +} + +// StepsKept is how many steps a put keeps, newest last. +const StepsKept = 16 + +// Put is the value of a key of the state puts. +type Put struct { + V int `json:"v"` + // FP is the fingerprint put, of the new form. + FP string `json:"fp"` + // Hold keeps the want at FP until released. + Hold bool `json:"hold,omitempty"` + Why string `json:"why"` + // By is who asked, as the bus named the caller; "unproven" when it named none. + By string `json:"by,omitempty"` + At time.Time `json:"at"` + // Step is where the put stands now, one of the steps above; Words say it in a sentence. + Step string `json:"step"` + Words string `json:"words,omitempty"` + // Composed is the digest of the composition the want names, once known. + Composed string `json:"composed,omitempty"` + // Ask is the key, in the asks state, of the ask about the move, once asked. + Ask string `json:"ask,omitempty"` + // Snapshot says where the bus's streams were snapshotted, once they were. + Snapshot string `json:"snapshot,omitempty"` + // Steps are the steps taken, oldest first, at most StepsKept. + Steps []Step `json:"steps,omitempty"` +} + +// Said is p at step with words, the step added to its steps when it differs from the last one. +func (p Put) Said(at time.Time, step, words string) Put { + if p.Step == step && p.Words == words { + return p + } + p.Step, p.Words = step, words + p.Steps = append(append([]Step(nil), p.Steps...), Step{At: at.UTC(), Step: step, Words: words}) + if len(p.Steps) > StepsKept { + p.Steps = p.Steps[len(p.Steps)-StepsKept:] + } + return p +} + +var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) + +func (p Put) check() error { + if _, err := meshcatalogclient.KeyPart(p.FP); err != nil { + return fmt.Errorf("puts: a put's fingerprint: %w", err) + } + if p.Why == "" { + return fmt.Errorf("puts: a put says why") + } + switch p.Step { + case Asked, Build, Compose, Person, Snapshot, Wanted, Holding, Done, Released, Refused: + default: + return fmt.Errorf("puts: %q is not a step of a put", p.Step) + } + if p.Composed != "" && !digestForm.MatchString(p.Composed) { + return fmt.Errorf("puts: a put's composition digest %q is not sha256:<64 lowercase hex digits>", p.Composed) + } + return nil +} + +// Key is the key, in the state puts, of the put of module on node: `.`. +func Key(node, module string) (string, error) { + return nec.HaveKey(node, module) +} + +// ParseKey reads a Key. +func ParseKey(key string) (node, module string, err error) { + node, module, isNode, err := nec.ParseHaveKey(key) + if err == nil && isNode { + err = fmt.Errorf("puts: %q is a node's own key, and no put", key) + } + return node, module, err +} + +// Encode is p as JSON; it refuses a fingerprint not of the new form, a put that says no why and an unknown step. +func Encode(p Put) ([]byte, error) { + if err := p.check(); err != nil { + return nil, err + } + p.V = keys.Version + return json.Marshal(p) +} + +// Decode reads a put; it refuses an unknown version (keys.ErrUnknownVersion) and what Encode refuses. +func Decode(data []byte) (Put, error) { + if err := keys.CheckVersion(data); err != nil { + return Put{}, err + } + var p Put + if err := json.Unmarshal(data, &p); err != nil { + return Put{}, fmt.Errorf("puts: value does not fit its shape: %w", err) + } + if err := p.check(); err != nil { + return Put{}, err + } + return p, nil +} diff --git a/modules/mesh-delivery-assigner/internal/puts/puts_test.go b/modules/mesh-delivery-assigner/internal/puts/puts_test.go new file mode 100644 index 00000000..5844ba3d --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/puts/puts_test.go @@ -0,0 +1,66 @@ +package puts + +import ( + "errors" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// TestPut holds a put's bytes equal to what go/delivery's codec wrote for the same value (golden), its steps kept and its +// key, and the refusals it made. +func TestPut(t *testing.T) { + a := "src2:" + strings.Repeat("a", 64) + at := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC) + p := Put{FP: a, Hold: true, Why: "why", By: "me", At: at, Composed: "sha256:" + strings.Repeat("c", 64), Ask: "k", Snapshot: "s"} + p = p.Said(at, Wanted, "w1") + p = p.Said(at, Wanted, "w1") + p = p.Said(at.Add(time.Minute), Holding, "w2") + golden := `{"v":1,"fp":"` + a + `","hold":true,"why":"why","by":"me","at":"2026-11-05T10:00:00Z","step":"holding","words":"w2","composed":"sha256:` + strings.Repeat("c", 64) + `","ask":"k","snapshot":"s","steps":[{"at":"2026-11-05T10:00:00Z","step":"wanted","words":"w1"},{"at":"2026-11-05T10:01:00Z","step":"holding","words":"w2"}]}` + raw, err := Encode(p) + if err != nil || string(raw) != golden { + t.Fatalf("wrote %s, %v", raw, err) + } + back, err := Decode(raw) + if err != nil || back.Step != Holding || len(back.Steps) != 2 { + t.Fatalf("read back %+v, %v", back, err) + } + for i := 0; i < 2*StepsKept; i++ { + p = p.Said(at, Build, strings.Repeat("x", i+1)) + } + if len(p.Steps) != StepsKept { + t.Fatalf("%d steps kept", len(p.Steps)) + } + if k, err := Key("ace", "novox.be"); err != nil || k != "ace.novox=2ebe" { + t.Fatalf("Key = %q %v", k, err) + } + if n, m, err := ParseKey("ace.novox=2ebe"); err != nil || n != "ace" || m != "novox.be" { + t.Fatalf("ParseKey = %q %q %v", n, m, err) + } + if _, _, err := ParseKey("ace.node"); err == nil { + t.Error("a node's own key is a put's") + } + for name, bad := range map[string]Put{ + "old fingerprint": {FP: "src1:x", Why: "w", Step: Asked}, + "no why": {FP: a, Step: Asked}, + "unknown step": {FP: a, Why: "w", Step: "later"}, + "bad digest": {FP: a, Why: "w", Step: Asked, Composed: "x"}, + } { + if _, err := Encode(bad); err == nil { + t.Errorf("%s: written", name) + } + } + if _, err := Decode([]byte(`{"fp":"` + a + `","why":"w","step":"asked"}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Errorf("no version: %v", err) + } + for _, s := range []string{Done, Released, Refused} { + if !Ended(s) { + t.Errorf("%s is not an end", s) + } + } + if Ended(Holding) { + t.Error("holding is an end") + } +}