diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go index f9ad00e9..55ced613 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go @@ -82,7 +82,7 @@ const ( tellerAnswers = "mesh-delivery-teller_answers" tellerAsked = "mesh-delivery-teller_asked" assignerAsks = "mesh-delivery-assigner_asks" - viewAnswers = viewstate.ViewAnswersBucket + viewAnswers = "mesh-delivery-view_" + viewstate.ViewAnswersState tellerConsumer = "n1_mesh-delivery-teller" seatAsk = "mesh.seat.operator-channel.accept.ask.mesh-delivery-teller" seatCancel = "mesh.seat.operator-channel.accept.cancel.mesh-delivery-teller" diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go index b31a8250..28b17f79 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go @@ -27,7 +27,7 @@ import ( func serve(t *testing.T) string { t.Helper() - m := view.New() + m := view.New(viewstate.ViewModule) placed, err := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}}) if err != nil { t.Fatal(err) @@ -82,7 +82,7 @@ func TestFailsLoudWhenTheViewIsNotThere(t *testing.T) { // 1134/AC6 } func TestPrintsTheCandidateGroupsAndTheFingerprintComparison(t *testing.T) { // 1241/AC6 - m := view.New() + m := view.New(viewstate.ViewModule) const head = "0123456789abcdef0123456789abcdef01234567" cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{Group: "feat/x", Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-delivery", Number: 12, Head: head, Modules: []string{"a"}}}, Heads: mustHash(t, meshdeliveryclient.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head})}) @@ -130,7 +130,7 @@ func mustHash(t *testing.T, h meshdeliveryclient.Head) string { // TestPrintsTheOpenAsksInTheTableAndTheJSON: the command shows what waits for the operator's Approve (1345/AC2). The ask is // the gate's, as mesh-sdk go/delivery EncodeAsk writes it under the key MergeAskKey gives (as internal/view asks_test.go). func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2 - m := view.New() + m := view.New(viewstate.ViewModule) key, err := asks.HoldAskKey("plex", "src2:2222222222222222222222222222222222222222222222222222222222222222") if err != nil { t.Fatal(err) @@ -219,7 +219,7 @@ func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b if err != nil { t.Fatal(err) } - m, w := view.New(), &putter{puts: map[string][]byte{}} + m, w := view.New(viewstate.ViewModule), &putter{puts: map[string][]byte{}} m.SetWriter(w) m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value}) socket := serveWith(t, m) @@ -262,7 +262,7 @@ func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) { hkey, _ := meshwantsclient.HeldKey("systemd-resolved", fp) held, _ := meshwantsclient.EncodeHeld(meshwantsclient.Held{Node: "ace", Words: "health failed, phase running: dns timeout"}) ans, _ := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "keep", Warrant: hold.ID + "@2026-10-25T04:00:00Z", Words: "the operator chose keep"}) - m, w := view.New(), &putter{puts: map[string][]byte{}} + m, w := view.New(viewstate.ViewModule), &putter{puts: map[string][]byte{}} m.SetWriter(w) m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value}) m.Apply(view.Entry{Bucket: view.AssignerWantsBucket, Key: hkey, Value: held}) diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go index 1ace4595..4ac60f21 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go @@ -4,13 +4,14 @@ import ( "context" "encoding/json" "fmt" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" "mesh-delivery-view/internal/view" "sort" "strings" "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go index da423d87..8b0057c9 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -9,11 +9,12 @@ import ( "testing" "time" + "mesh-delivery-view/internal/view" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" - "mesh-delivery-view/internal/view" ) // encoded is v as its contract's encoder writes it, failing the test when it refuses. diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go index b88736d9..6407e352 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go @@ -564,7 +564,7 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129 return kv } askBucket := bucket(view.AssignerAsksBucket, view.AssignerAsksHistory) - viewBucket := bucket(viewstate.ViewAnswersBucket, viewstate.ViewAnswersHistory) + viewBucket := bucket(viewstate.BucketsOf(viewstate.ViewModule).Answers, viewstate.ViewAnswersHistory) bucket(viewstate.ViewModule+"_"+views.MarksState, 1) teller := bucket(view.AnswersBucket, view.AnswersHistory) // The view says its answers on its views family (design 47 G A-178), which the teller follows. @@ -788,7 +788,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) { t.Fatal(err) } kvs := map[string]jetstream.KeyValue{} - for _, name := range []string{view.AssignerWantsBucket, view.HaveBucket, viewstate.ReachBucket} { + for _, name := range []string{view.AssignerWantsBucket, view.HaveBucket, viewstate.BucketsOf(viewstate.ViewModule).Reach} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 5}) if err != nil { t.Fatal(err) @@ -860,7 +860,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) { if err := json.Unmarshal(rm.Data, &re); err != nil || re.Key != "novox/web" { t.Fatalf("the event reached = %s, %v", rm.Data, err) } - if stored, gerr := kvs[viewstate.ReachBucket].Get(ctx, re.Key); gerr != nil || string(stored.Value()) != string(re.Reach) { + if stored, gerr := kvs[viewstate.BucketsOf(viewstate.ViewModule).Reach].Get(ctx, re.Key); gerr != nil || string(stored.Value()) != string(re.Reach) { t.Errorf("the event carries %s, the state reach holds %v (%v)", re.Reach, stored, gerr) } } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go index 65a5b543..c85c073c 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go @@ -66,7 +66,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro web = defaultWeb } - model := view.New() + model := view.New(cred.Module) + own := viewstate.BucketsOf(cred.Module) // its own buckets, named from its credential's module (A-296) l, err := view.ListenSocket(socket) if err != nil { return fmt.Errorf("cannot listen on %s: %w", socket, err) @@ -81,7 +82,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err } defer conn.Close() - model.SetWriter(kvWriter{ctx: ctx, js: js}) + model.SetWriter(kvWriter{ctx: ctx, js: js, bucket: own.Answers}) model.SetMotionEvent(func(e view.MotionEvent) error { err := emitEvent(ctx, js, cred, e.Type, e.Body, e.ID) if err != nil { @@ -106,7 +107,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err }) model.SetReachWriter(func(w view.ReachWrite) error { - rev, err := putReach(ctx, js, w) + rev, err := putReach(ctx, js, own.Reach, w) if err != nil { logger.Printf("the reach of %s could not be written (written again at the next change the view takes in): %v", w.Key, err) return err @@ -154,13 +155,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } } open := buckets{js: js, reader: reader} - for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket, viewstate.ReachBucket} { + for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.AssignerAsksBucket, own.Answers, view.TakenBucket, own.Reach} { go follow(ctx, open, bucket, false, model, logger) } // The view's own state reach, as it stood at the start, is what the derivation lays its history over (design 47 G A-225); // the modules' history is each revision of the catalog half the view takes, in its order (below), and each watched // branch's source is the seat mesh-delivery's verb source per branch, followed by its source-moved (A-173). - statusBucket := process.AwaitBucket(ctx, js, conn, firstViewsBucket, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf) + statusBucket := process.AwaitBucket(ctx, js, conn, cred.Module+"_"+views.StatusState, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf) // The modules are the catalog half's, per watched branch, through meshcatalogclient's branch follower adopted on the // reader (design 47 G §532, §614): the registrations' branches, before the first registrations the gate's source's; // each branch's first view, its status, its hand asks and its event catalog-revision are the reader's. A module's entry @@ -174,7 +175,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro modules := &catalogModules{ctx: ctx, branches: branches, sources: sources, model: model, logger: logger, applied: map[string]bool{}} branches.Changed = modules.changed reader.Start(ctx, js, cred, statusBucket) - if err := hearWaits(ctx, hub, js, logger.Printf); err != nil { + if err := hearWaits(ctx, hub, js, own.Waits, logger.Printf); err != nil { return fmt.Errorf("the waits' events are not handled: %w", err) } if err := hub.StartUntilBound(ctx, js, cred, nil, nil); err != nil { @@ -207,10 +208,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // by view_read_answers and that family, never by reading the state. A watch that ends is sent on errs, and the program ends // with it, loudly. func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, cred process.Account, writer views.WireWriter, errs chan<- error, logf func(string, ...any)) { - marks := viewstate.ViewModule + "_" + views.MarksState + marks := cred.Module + "_" + views.MarksState process.AwaitBucket(ctx, js, nc, marks, "the view's answers are not said on its views family, so the teller waits on its first view", logf).Ready(func(mk jetstream.KeyValue) { em := views.Emitter{Owner: viewstate.ViewModule, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, Findings: writer, Logf: logf} - process.AwaitBucket(ctx, js, nc, viewstate.ViewAnswersBucket, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) { + process.AwaitBucket(ctx, js, nc, viewstate.BucketsOf(cred.Module).Answers, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) { if err := em.Start(ctx, viewstate.ViewAnswersState, kv, errs); err != nil { logf("%v: the teller asks view_read_answers at each change until the view starts again", err) } @@ -221,24 +222,25 @@ func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscrib // kvWriter is the view's one write: a key of its own bucket answers (manifest state entry answers), which the teller reads. // The bucket is the controller's to create; a bucket not there yet is an error the operator is told. type kvWriter struct { - ctx context.Context - js jetstream.JetStream + ctx context.Context + js jetstream.JetStream + bucket string } func (w kvWriter) Put(key string, value []byte) error { - kv, err := w.js.KeyValue(w.ctx, viewstate.ViewAnswersBucket) + kv, err := w.js.KeyValue(w.ctx, w.bucket) if err != nil { - return fmt.Errorf("the bucket %s: %w", viewstate.ViewAnswersBucket, err) + return fmt.Errorf("the bucket %s: %w", w.bucket, err) } _, err = kv.Put(w.ctx, key, value) return err } // putReach writes a reach value under its key in the view's own state reach (ADR 0556 clause 2), its only writer. -func putReach(ctx context.Context, js jetstream.JetStream, w view.ReachWrite) (uint64, error) { - kv, err := js.KeyValue(ctx, viewstate.ReachBucket) +func putReach(ctx context.Context, js jetstream.JetStream, bucket string, w view.ReachWrite) (uint64, error) { + kv, err := js.KeyValue(ctx, bucket) if err != nil { - return 0, fmt.Errorf("the bucket %s: %w", viewstate.ReachBucket, err) + return 0, fmt.Errorf("the bucket %s: %w", bucket, err) } return kv.Put(ctx, w.Key, w.Value) } @@ -253,9 +255,6 @@ type ReachedEvent struct { Reach json.RawMessage `json:"reach"` } -// The view's own state of its first views (views.StatusState). -var firstViewsBucket = viewstate.ViewModule + "_" + views.StatusState - // ownersFollowed are the indexer's, the assigner's and the teller's states the view's process follows by their first views, each by its // owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the // view's tools ask the other six through the owners' read tools on each call. diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/waits.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/waits.go index 0b799309..f96014bc 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/waits.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/waits.go @@ -56,10 +56,10 @@ func applyWait(subject string, body []byte, put func(key string, value []byte) e return fmt.Errorf("%s is no wait's subject", subject) } -// hearWaits registers the waits' handlers on the process's hub; each keeps its event in the state waits and acknowledges it, and +// hearWaits registers the waits' handlers on the process's hub, the state waits its bucket; each keeps its event in the state waits and acknowledges it, and // one that could not be kept is not acknowledged, so the bus gives it again. -func hearWaits(ctx context.Context, hub *process.EventHub, js jetstream.JetStream, logf func(string, ...any)) error { - kv := func() (jetstream.KeyValue, error) { return js.KeyValue(ctx, viewstate.WaitsBucket) } +func hearWaits(ctx context.Context, hub *process.EventHub, js jetstream.JetStream, bucket string, logf func(string, ...any)) error { + kv := func() (jetstream.KeyValue, error) { return js.KeyValue(ctx, bucket) } put := func(key string, value []byte) error { b, err := kv() if err != nil { diff --git a/modules/mesh-delivery-view/internal/view/answers_test.go b/modules/mesh-delivery-view/internal/view/answers_test.go index 9ef5ef79..98406ab2 100644 --- a/modules/mesh-delivery-view/internal/view/answers_test.go +++ b/modules/mesh-delivery-view/internal/view/answers_test.go @@ -53,7 +53,7 @@ func holdAsk(t *testing.T) (Entry, oc.Ask) { } func TestAnAskNeedingNoProofIsOpenInTheViewWithItsOptions(t *testing.T) { // 1297/B9 - m := New() + m := New(viewstate.ViewModule) e, a := holdAsk(t) fill(m, e) got := m.Snapshot().Asks @@ -68,7 +68,7 @@ func TestAnAskNeedingNoProofIsOpenInTheViewWithItsOptions(t *testing.T) { // 129 } func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a AC - m, w := New(), &memWriter{} + m, w := New(viewstate.ViewModule), &memWriter{} m.SetWriter(w) e, _ := holdAsk(t) fill(m, e) @@ -88,7 +88,7 @@ func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a A t.Errorf("parse = %q, %v", back, err) } // The view hears its own answer back from its bucket and shows the ask as answered, waiting for the teller. - fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, Key: e.Key + ".view", Value: raw, At: t0}) + fill(m, Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Answers, Key: e.Key + ".view", Value: raw, At: t0}) got := m.Snapshot().Asks if len(got) != 1 || got[0].Chosen != "release" { t.Errorf("asks = %+v, want the ask with chosen release", got) @@ -113,7 +113,7 @@ func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC {"no option", e.Key, " ", "no option", []Entry{e}}, } for _, c := range cases { - m, w := New(), &memWriter{} + m, w := New(viewstate.ViewModule), &memWriter{} m.SetWriter(w) fill(m, c.prep...) err := m.Answer(c.key, c.option, "") @@ -124,7 +124,7 @@ func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC t.Errorf("%s: wrote %v", c.name, w.puts) } } - m := New() + m := New(viewstate.ViewModule) e2, _ := holdAsk(t) fill(m, e2) if err := m.Answer(e2.Key, "release", ""); err == nil || !strings.Contains(err.Error(), "cannot write") { @@ -182,7 +182,7 @@ func postAnswer(h http.Handler, form url.Values, mod func(*http.Request)) *httpt } func TestThePageAnswersAnAskAndRefusesAForeignPost(t *testing.T) { // 1297/B9a - m, w := New(), &memWriter{} + m, w := New(viewstate.ViewModule), &memWriter{} m.SetWriter(w) e, _ := holdAsk(t) fill(m, e) @@ -226,14 +226,14 @@ func TestThePageAnswersAnAskAndRefusesAForeignPost(t *testing.T) { // 1297/B9a } func TestAStaleViewAnswerOfAnotherAskingIsNotTheChoice(t *testing.T) { // 1297/B9 (stale answer) - m := New() + m := New(viewstate.ViewModule) e, a := holdAsk(t) fill(m, e) stale, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: "an-earlier-asking", Option: "release"}) if err != nil { t.Fatal(err) } - fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, Key: e.Key + ".view", Value: stale, At: t0}) + fill(m, Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Answers, Key: e.Key + ".view", Value: stale, At: t0}) if got := m.Snapshot().Asks; len(got) != 1 || got[0].Chosen != "" { t.Errorf("a stale answer shows as the choice: %+v", got) } diff --git a/modules/mesh-delivery-view/internal/view/asks_test.go b/modules/mesh-delivery-view/internal/view/asks_test.go index 54ca5a90..5c7d046f 100644 --- a/modules/mesh-delivery-view/internal/view/asks_test.go +++ b/modules/mesh-delivery-view/internal/view/asks_test.go @@ -11,6 +11,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" ) // answerEntry is the teller's answer to the first asking of the key; answerToAsking to the asking with this router id. The @@ -29,7 +30,7 @@ func answerToAsking(t *testing.T, key, id string) Entry { } func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1 - m := New() + m := New(viewstate.ViewModule) e, a := holdAsk(t) fill(m, e) s := m.Snapshot() @@ -54,7 +55,7 @@ func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/ // The gate's asks go with the Approve per merge (design 47 G §431, §512): a value under the gate's former state is no ask // of the view's, and nothing names it unreadable. func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) fill(m, Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.g.0123456789ab", Value: []byte(`{"v":1,"group":"g"}`), At: t0}, Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.h.0123456789ab", Value: []byte("{"), At: t0}) if s := m.Snapshot(); len(s.Asks) != 0 || len(s.Unreadable) != 0 { @@ -63,7 +64,7 @@ func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) { } func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) e, _ := holdAsk(t) fill(m, answerEntry(t, e.Key), e) if got := m.Snapshot().Asks; len(got) != 0 { @@ -72,7 +73,7 @@ func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) { } func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped silently - m := New() + m := New(viewstate.ViewModule) e, _ := holdAsk(t) fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Value: []byte("{"), At: t0}) s := m.Snapshot() @@ -82,7 +83,7 @@ func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped sil } func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2 - m := New() + m := New(viewstate.ViewModule) e, a := holdAsk(t) fill(m, e) var out bytes.Buffer @@ -101,7 +102,7 @@ func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2 } // No ask, no section. out.Reset() - Table(&out, New().Snapshot()) + Table(&out, New(viewstate.ViewModule).Snapshot()) if strings.Contains(out.String(), "ASKS") { t.Errorf("the table shows an ASKS section with no ask:\n%s", out.String()) } @@ -121,7 +122,7 @@ func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2 } func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed - m := New() + m := New(viewstate.ViewModule) fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", nodeengineclient.PhaseRemoved, nodeengineclient.HealthOK, ""), t0), haveE(t, "novox", "stray", have(t, "b2", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0)) s := m.Snapshot() diff --git a/modules/mesh-delivery-view/internal/view/backlog_test.go b/modules/mesh-delivery-view/internal/view/backlog_test.go index a8fc1687..13049161 100644 --- a/modules/mesh-delivery-view/internal/view/backlog_test.go +++ b/modules/mesh-delivery-view/internal/view/backlog_test.go @@ -9,6 +9,7 @@ import ( "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "github.com/nats-io/nats.go/jetstream" ) @@ -53,7 +54,7 @@ func fakeSeats(t *testing.T, now time.Time) *Backlog { func TestThreeWaitingAsksAreShownWithTheOldestsAge(t *testing.T) { // 1887/A8a AC now := time.Date(2026, 10, 23, 12, 0, 0, 0, time.UTC) - m := New() + m := New(viewstate.ViewModule) m.SetBacklog(fakeSeats(t, now)) rows := m.Snapshot().Backlog var check, image *BacklogRow @@ -88,7 +89,7 @@ func TestThreeWaitingAsksAreShownWithTheOldestsAge(t *testing.T) { // 1887/A8a A } func TestNoBacklogReaderGivesNoRows(t *testing.T) { - if got := New().Snapshot().Backlog; got == nil || len(got) != 0 { + if got := New(viewstate.ViewModule).Snapshot().Backlog; got == nil || len(got) != 0 { t.Errorf("backlog = %#v", got) } } diff --git a/modules/mesh-delivery-view/internal/view/fingerprints_newform_test.go b/modules/mesh-delivery-view/internal/view/fingerprints_newform_test.go index c39adafd..b68f8be2 100644 --- a/modules/mesh-delivery-view/internal/view/fingerprints_newform_test.go +++ b/modules/mesh-delivery-view/internal/view/fingerprints_newform_test.go @@ -6,6 +6,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" ) // I5 (issue 1851): from R4e the indexer writes the new form alone, and a module's fingerprint is built by the builder's key @@ -27,7 +28,7 @@ func builderKey(t *testing.T, module, fp2 string, b builds.BuilderBuild) Entry { } func TestAFingerprintOfTheNewFormIsShownAgainstItsBuilderKey(t *testing.T) { // I5 - m := New() + m := New(viewstate.ViewModule) manifest := strings.Repeat("a", 64) d := "sha256:" + strings.Repeat("2", 64) fill(m, diff --git a/modules/mesh-delivery-view/internal/view/fingerprints_test.go b/modules/mesh-delivery-view/internal/view/fingerprints_test.go index 3a937fe8..df74d8dc 100644 --- a/modules/mesh-delivery-view/internal/view/fingerprints_test.go +++ b/modules/mesh-delivery-view/internal/view/fingerprints_test.go @@ -12,6 +12,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" ) const ( @@ -66,7 +67,7 @@ func fpRow(t *testing.T, s Snapshot, module string) FingerprintRow { } func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1 - m := New() + m := New(viewstate.ViewModule) fill(m, candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a", "b"), pullOf("novox/mesh-catalog", 300, hB, "c")), candidate(t, "feat/y", pullOf("novox/mesh-delivery", 13, hB))) @@ -94,7 +95,7 @@ func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1 // 1955/L7 (ADR 0571 §1): the view reads no build record of the controller's: a module with no fingerprint of the new // form is said so, never compared with the controller's builds. func TestAModuleOfTheOldFormIsComparedWithNoBuild(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) fill(m, modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp: fF, Repository: "novox/mesh-delivery", Commit: hA}), modEntry(t, "b", meshcatalog.ModulesByModuleValue{None: "an artifact compiles a Go program from a build source that git trees do not pin: no fingerprint before release 4", Repository: "novox/mesh-delivery", Commit: hA})) @@ -107,7 +108,7 @@ func TestAModuleOfTheOldFormIsComparedWithNoBuild(t *testing.T) { } func shown(t *testing.T) *Model { - m := New() + m := New(viewstate.ViewModule) fill(m, candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a"), pullOf("novox/mesh-catalog", 300, hB, "c")), modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Repository: "novox/mesh-delivery", Commit: hA}), @@ -135,7 +136,7 @@ func TestTableAndPagePrintTheGroupsAndTheComparison(t *testing.T) { // 1241/AC6 } func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silently - m := New() + m := New(viewstate.ViewModule) fill(m, Entry{Bucket: CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0}, Entry{Bucket: ModulesBucket, Key: "a", Value: []byte("{"), At: t0}) s := m.Snapshot() diff --git a/modules/mesh-delivery-view/internal/view/holds_test.go b/modules/mesh-delivery-view/internal/view/holds_test.go index 910456d6..3374623b 100644 --- a/modules/mesh-delivery-view/internal/view/holds_test.go +++ b/modules/mesh-delivery-view/internal/view/holds_test.go @@ -36,7 +36,7 @@ func answered(t *testing.T, key, id, option string) Entry { // 1906/N23-AC1: given holds in the assigner's wants, the view lists each with its node, its cause in words, its hold ask and // the answer that stands for that asking; a hold without an ask says so, and a held key is never taken for a want. func TestTheViewListsEveryHoldWithItsNodeCauseAndAnswer(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) e, a := holdAsk(t) fill(m, e, heldE(t, "plex", fpNew, "ace", "health failed, phase running: connection refused"), answered(t, e.Key, a.ID, "keep"), heldE(t, "web", fpNew, "g14", "health failed")) @@ -60,7 +60,7 @@ func TestTheViewListsEveryHoldWithItsNodeCauseAndAnswer(t *testing.T) { // writes its answer "release" to that asking under the ask's view key, though the ask was answered; the fingerprint may be the // start of its hex. The same answer again, an unknown hold and another option are refused, and write nothing. func TestAKeptHoldCanBeReleased(t *testing.T) { - m, w := New(), &memWriter{} + m, w := New(viewstate.ViewModule), &memWriter{} m.SetWriter(w) e, a := holdAsk(t) fill(m, e, heldE(t, "plex", fpNew, "ace", "health failed"), answered(t, e.Key, a.ID, "keep")) diff --git a/modules/mesh-delivery-view/internal/view/reach.go b/modules/mesh-delivery-view/internal/view/reach.go index 92c8b245..701b5553 100644 --- a/modules/mesh-delivery-view/internal/view/reach.go +++ b/modules/mesh-delivery-view/internal/view/reach.go @@ -28,8 +28,9 @@ func ReachHistory(bucket string) string { return bucket + "#history" } // reachBuckets are the watches the derivation waits for before it writes or says anything, so a start does not write a reach // derived from half its inputs; the view's own reach first, which a restart lays the derivation over (A-225). -var reachBuckets = []string{ModulesBucket, AssignerWantsBucket, HaveBucket, TakenBucket, ReachHistory(ModulesBucket), - viewstate.ReachBucket} +func (m *Model) reachBuckets() []string { + return []string{ModulesBucket, AssignerWantsBucket, HaveBucket, TakenBucket, ReachHistory(ModulesBucket), m.own.Reach} +} // ReachWrite is a reach value to write under its key in the view's state reach. type ReachWrite struct { @@ -380,7 +381,7 @@ func short(fp string) string { // reachReady is whether every watch the derivation reads handed over what stood at its start. func (m *Model) reachReady() bool { - for _, b := range reachBuckets { + for _, b := range m.reachBuckets() { if !m.initial[b] { return false } diff --git a/modules/mesh-delivery-view/internal/view/reach_test.go b/modules/mesh-delivery-view/internal/view/reach_test.go index 69ebee73..54e10c41 100644 --- a/modules/mesh-delivery-view/internal/view/reach_test.go +++ b/modules/mesh-delivery-view/internal/view/reach_test.go @@ -65,7 +65,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { if err != nil { t.Fatal(err) } - return Entry{Bucket: viewstate.ReachBucket, Key: key, Value: v, At: t0} + return Entry{Bucket: viewstate.BucketsOf(viewstate.ViewModule).Reach, Key: key, Value: v, At: t0} } taken := func(repository, merge string) Entry { key, _ := trunk.RepositoryKey(repository) @@ -167,7 +167,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { then: []Entry{haveFP(t, "ace", "mail", fpMail, nodeengineclient.HealthOK)}}, } { t.Run(tc.name, func(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) var written []ReachWrite var said []DeliveredEvent refuse := tc.refused @@ -192,7 +192,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { if len(written) != 0 { t.Fatalf("written before every watch handed over: %v", written) } - for _, b := range reachBuckets { + for _, b := range m.reachBuckets() { m.Initial(b) } refuse = false diff --git a/modules/mesh-delivery-view/internal/view/serve_test.go b/modules/mesh-delivery-view/internal/view/serve_test.go index 632ff2e2..246bfb0b 100644 --- a/modules/mesh-delivery-view/internal/view/serve_test.go +++ b/modules/mesh-delivery-view/internal/view/serve_test.go @@ -12,11 +12,12 @@ import ( "testing" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" ) func served(t *testing.T) (string, *Model) { t.Helper() - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0)) dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "s") @@ -113,7 +114,7 @@ func TestWebIsTheSameRowsAndReadOnly(t *testing.T) { // 1134/AC6 func watchmanSnapshot(t *testing.T) *Model { t.Helper() - m := New() + m := New(viewstate.ViewModule) fill(m, presenceE(t, "a", false, t0), stuckE(t, "node-engine_have", "a.x", "a report from a", t0, false), Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0}) return m diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index cfc4a2e9..ea977abe 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -236,6 +236,7 @@ type UnreadableRow struct { // Model holds the rows' inputs in memory and nothing else: a restart fills it again from the buckets' last values. type Model struct { mu sync.Mutex + own viewstate.Buckets // its own buckets, named from its credential's module (A-296) placed *meshmoduleassignerclient.Placed // the seat's assignments (the assigner's state, ADR 0567, ADR 0549 §3) builds map[string]buildEntry // by .build. groups map[string]groupEntry // the indexer's candidates, by group @@ -364,9 +365,9 @@ type nodeEntry struct { at time.Time } -// New is an empty model. -func New() *Model { - return &Model{placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{}, +// New is an empty model of the view module (its credential's). +func New(module string) *Model { + return &Model{own: viewstate.BucketsOf(module), placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{}, haves: map[string]haveEntry{}, nodes: map[string]nodeEntry{}, presence: map[string]presenceEntry{}, stuck: map[string]stuckEntry{}, modHist: map[string][]meshcatalog.ModulesByModuleValue{}, srcHist: map[string]map[uint64]map[string]string{}, kept: map[string]viewstate.Reach{}, @@ -582,7 +583,7 @@ func (m *Model) apply(e Entry) { } a, err := decodeAssignerAsk(e.Key, e.Value) m.aasks[e.Key] = assignerAskRecord{ask: a, err: errText(err)} - case viewstate.ViewAnswersBucket: + case m.own.Answers: // the view's own state answers askKey, err := viewstate.ParseViewAnswerKey(e.Key) if err != nil { return @@ -640,8 +641,8 @@ func (m *Model) apply(e Entry) { } m.srcHist[e.Key][e.Revision] = src.Heads } - case viewstate.ReachBucket: // the view's own state reach, as it stood at the start (A-225) - if m.initial[viewstate.ReachBucket] { + case m.own.Reach: // the view's own state reach, as it stood at the start (A-225) + if m.initial[m.own.Reach] { return } if r, err := viewstate.DecodeReach(e.Value); err == nil && !e.Deleted { diff --git a/modules/mesh-delivery-view/internal/view/view_test.go b/modules/mesh-delivery-view/internal/view/view_test.go index ff52bf09..22853fac 100644 --- a/modules/mesh-delivery-view/internal/view/view_test.go +++ b/modules/mesh-delivery-view/internal/view/view_test.go @@ -18,6 +18,7 @@ import ( healers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/motion" ) @@ -82,7 +83,7 @@ func base(t *testing.T) []Entry { } func TestRowsPerAssignedModuleAndNode(t *testing.T) { // 1134/AC1 - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) s := m.Snapshot() if len(s.Rows) != 4 || s.Counted() != 4 { @@ -101,14 +102,14 @@ func TestRowsPerAssignedModuleAndNode(t *testing.T) { // 1134/AC1 } func TestLeftBehindIsWhatTheAssignmentsDoesNotPut(t *testing.T) { // 1134/AC3, issue 774 (the seat's assignments) - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) fill(m, place(t, "b", "x")) // z taken off b s := m.Snapshot() if r := find(t, s, "b", "z"); r.Status != StatusLeftBehind || s.Counted() != 3 { t.Errorf("status = %q counted %d, want %q and 3", r.Status, s.Counted(), StatusLeftBehind) } - m = New() + m = New(viewstate.ViewModule) fill(m, base(t)...) fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0)) // placed nowhere s = m.Snapshot() @@ -122,7 +123,7 @@ func TestLeftBehindIsWhatTheAssignmentsDoesNotPut(t *testing.T) { // 1134/AC3, i } func TestUnknownVersionSaysSo(t *testing.T) { // 1134/AC4 - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) before := m.Snapshot() fill(m, haveE(t, "a", "x", []byte(`{"v":9,"phase":"running","health":"ok"}`), t0)) @@ -146,7 +147,7 @@ func TestUnknownVersionSaysSo(t *testing.T) { // 1134/AC4 } func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5 - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) before := m.Snapshot() fill(m, haveE(t, "b", "x", have(t, "b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0.Add(9*time.Minute))) @@ -174,7 +175,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5 t.Fatal("a deleted have must read as no have") } // a restart rebuilds the same rows from last values - a, b := New(), New() + a, b := New(viewstate.ViewModule), New(viewstate.ViewModule) es := base(t) fill(a, es...) fill(b, es...) @@ -184,7 +185,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5 } func TestNodeRow(t *testing.T) { // 1134/AC1 (the node's own key gives a node row) - m := New() + m := New(viewstate.ViewModule) v, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Windows: []map[string]any{{}, {}}, Busy: []map[string]any{{"a": 1}}, Strays: []map[string]any{}}) fill(m, Entry{Bucket: HaveBucket, Key: "a.node", Value: v, At: t0}) n := m.Snapshot().Nodes @@ -218,7 +219,7 @@ func TestTheWatchmansKeysAndStatus(t *testing.T) { } func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2 - m := New() + m := New(viewstate.ViewModule) fill(m, base(t)...) fill(m, presenceE(t, "a", false, t0), @@ -246,7 +247,7 @@ func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2 } func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3 - m := New() + m := New(viewstate.ViewModule) fill(m, Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9,"online":false}`), At: t0}, Entry{Bucket: StuckBucket, Key: "node-engine_have.c.x", Value: []byte(`{"v":9}`), At: t0}, diff --git a/modules/mesh-delivery-view/internal/view/wants_test.go b/modules/mesh-delivery-view/internal/view/wants_test.go index c6e2f0e6..695ba009 100644 --- a/modules/mesh-delivery-view/internal/view/wants_test.go +++ b/modules/mesh-delivery-view/internal/view/wants_test.go @@ -14,6 +14,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -87,7 +88,7 @@ func sourceRowOf(m *Model, branch string) *SourceRow { // 1906/D7 AC7: every want is a row beside the have of its node, with where it stands. func TestEveryWantIsARowAgainstItsHave(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "mail", fpOld, nodeengineclient.HealthOK), @@ -123,7 +124,7 @@ func TestEveryWantIsARowAgainstItsHave(t *testing.T) { // 1906/D7 AC7: a source revision is delivered when every want equals its module's fingerprint and every have equals its // want, healthy; one failed have shows it not delivered and names the module. func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) var said []DeliveredEvent m.SetDeliveredEvent(func(e DeliveredEvent) error { if e.Repository == "" { @@ -183,13 +184,13 @@ func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) { } func TestNoSourceRevisionHeldMeansNoStateIsDerived(t *testing.T) { - if s := New().Snapshot().Sources; len(s) != 0 { + if s := New(viewstate.ViewModule).Snapshot().Sources; len(s) != 0 { t.Errorf("source = %+v", s) } } func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK)) text := wantText(m.Snapshot()) if len(text) != 1 || text[0][0] != "ace" || text[0][1] != "web" || text[0][2] != fpNew || text[0][3] != fpOld || text[0][4] != WantPending || text[0][5] != "none" { @@ -201,7 +202,7 @@ func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) { func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) { // 1955/L7 (ADR 0571 §6): the judgement is the node agent's, in its have waiting for a yes; the controller's composed copy // is not read. - m := New() + m := New(viewstate.ViewModule) h, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Phase: nodeengineclient.PhaseWaitingYes, Health: nodeengineclient.HealthOK, Judgement: nodeengineclient.JudgedCannot, Waits: "sha256:" + strings.Repeat("c", 64), Detail: "the composition it runs is gone"}) if err != nil { @@ -221,7 +222,7 @@ func TestTheJudgementOfTheMoveIsShownBesideTheWant(t *testing.T) { // 1906/D9 (asked by the lead for issue 1946): the view says, once for each source revision, that it is delivered: the event the // issue tracker derives "fix delivered" from, in place of the gate's states that go in release 6 (ADR 0539 §17). func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) var said []DeliveredEvent refuse := true // the bus refuses the first try to say it (issue 1126): it is said at the next change, not lost m.SetDeliveredEvent(func(e DeliveredEvent) error { @@ -267,7 +268,7 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) { // Review round 1 (view notes): the controller no longer writes the build of an assignment (C6), so a row reads the want and the // have: the row shows the fingerprint wanted and waits while the node runs another. func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) fill(m, place(t, "ace", "web"), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK)) r := find(t, m.Snapshot(), "ace", "web") if r.Assigned != fpNew || r.Have != fpOld || !strings.Contains(r.Waits, "has not applied") { @@ -282,7 +283,7 @@ func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) { // Review round 1 (view notes, issue 1150): a module whose name holds a dot has its have under the escaped key; the row finds it, // and a have nothing is assigned to keeps the module's name. func TestADottedModuleNameIsFoundAndKept(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) // The seat's assignments refuses a dotted module name (mesh-sdk meshmoduleassigner.Assignments), so such a module is never placed: // its have is a row left behind under its own name. fill(m, place(t, "ace"), wantE(t, "ace", "novox.be", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "novox.be", fpNew, nodeengineclient.HealthOK), @@ -320,7 +321,7 @@ func types(said []MotionEvent) string { } func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) said := motionLog(m) fill(m, haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK)) if len(*said) != 0 { @@ -364,7 +365,7 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) { } func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) said := motionLog(m) fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew})) b, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Phase: nodeengineclient.PhaseHeld, Health: nodeengineclient.HealthWaits, Detail: "no space left"}) @@ -377,7 +378,7 @@ func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) { t.Errorf("stuck said twice: %q", types(*said)) } // failed on the wanted fingerprint - m2 := New() + m2 := New(viewstate.ViewModule) said2 := motionLog(m2) fill(m2, wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthFailed)) if types(*said2) != "moving,stuck" || (*said2)[1].Body.Outcome != "failed" { @@ -386,7 +387,7 @@ func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) { } func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) said := motionLog(m) fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK)) sk, _ := meshwatchdogsclient.StuckKey(AssignerWantsBucket, "want.ace.web") @@ -401,7 +402,7 @@ func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T) } func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) said := motionLog(m) fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew})) if len(*said) != 0 { @@ -412,7 +413,7 @@ func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) { // Nothing is said of a module in motion before the buckets have been read: a want read ahead of its have is not a node behind. What is // in motion when they have been read is said then, and a module that settles after is said settled. func TestMotionIsSaidOnlyOnceTheBucketsHaveBeenRead(t *testing.T) { - m := New() + m := New(viewstate.ViewModule) var said []MotionEvent m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil }) fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew})) diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 37f76567..6237f0d2 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -3,7 +3,6 @@ package manifest import ( "bytes" "encoding/json" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" "io/fs" "mesh-delivery-view/internal/view" "os" @@ -14,6 +13,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-sdk/go/keys" @@ -256,8 +257,19 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 t.Errorf("the manifest names %s: the modules are the catalog half's (ADR 0663)", name) } } - if viewstate.ViewModule != m.Module || viewstate.ViewAnswersBucket != m.Module+"_answers" || viewstate.ReachBucket != m.Module+"_reach" { - t.Errorf("the view names itself %s (%s, %s), its manifest %s", viewstate.ViewModule, viewstate.ViewAnswersBucket, viewstate.ReachBucket, m.Module) + if viewstate.ViewModule != m.Module { + t.Errorf("the view names itself %s, its manifest %s", viewstate.ViewModule, m.Module) + } + // A-296: its own buckets are named from its credential's module, each a state its manifest keeps. + var own []string + for _, s := range m.State { + own = append(own, "a-view_"+s.Name) + } + b := viewstate.BucketsOf("a-view") + for _, want := range []string{b.Answers, b.Reach, b.Waits} { + if !slices.Contains(own, want) { + t.Errorf("the view keeps %s, its manifest's states are %v", want, own) + } } // The packages key is the seat's key of its state packages for a module (), as the indexer writes it: the // escaped path, with no dot, so a reader never takes it for a . key. diff --git a/modules/mesh-delivery-view/viewstate/reach.go b/modules/mesh-delivery-view/viewstate/reach.go index 1c6bb4ca..031c3937 100644 --- a/modules/mesh-delivery-view/viewstate/reach.go +++ b/modules/mesh-delivery-view/viewstate/reach.go @@ -22,12 +22,20 @@ const modulesHistory = 20 // How far a repository reached (novox/hq ADR 0556, issue 1280). The indexer says, per repository, the last merge it took in // (the indexer's state `taken`); the delivery view keeps, per repository, how far each of its modules reached on the nodes that want it, and -// derives from it the repository's reached head (ReachBucket): the newest head of the repository at which every wanted +// derives from it the repository's reached head (its state reach): the newest head of the repository at which every wanted // module runs, on every node, a build at or after its last change. A merge at or before the reached head is on the // machines. Nothing records a merge as delivered (ADR 0429 §1): the gate and the tracker derive it from this state. -// ReachBucket is the delivery view's state `reach`: one key per repository (RepositoryKey), how far its modules reached. -const ReachBucket = ViewModule + "_reach" +// ReachState is the delivery view's state `reach`: one key per repository (RepositoryKey), how far its modules reached. +const ReachState = "reach" + +// Buckets are the delivery view's own state buckets, `_`: waits, reach and answers. +type Buckets struct{ Waits, Reach, Answers string } + +// BucketsOf names the view's own buckets from module, its credential's, never a constant (design 47 G A-296). +func BucketsOf(module string) Buckets { + return Buckets{Waits: module + "_" + WaitsState, Reach: module + "_" + ReachState, Answers: module + "_" + ViewAnswersState} +} // Reach is the value of a reach key: how far the modules of one repository reached on the nodes. Indexed is the last // merge the indexer took in for it (Taken.Merge), empty while it took in none. A repository the indexer holds no module @@ -133,11 +141,10 @@ func SameReach(a, b Reach) bool { return true } -// ViewAnswersBucket is the delivery view's state `answers`: one key per ask it answered, and ViewAnswersHistory the revisions it +// ViewAnswersState is the delivery view's state `answers`: one key per ask it answered, and ViewAnswersHistory the revisions it // keeps per key. const ( ViewAnswersState = "answers" - ViewAnswersBucket = ViewModule + "_" + ViewAnswersState ViewAnswersHistory = 5 // ViewAnswersReadTool is the view's read tool of its state answers (mesh-sdk views.ReadTool), whose changes it says on its // views family as views.answers; its marks are its state view-marks (views.MarksState) (design 47 G A-178). diff --git a/modules/mesh-delivery-view/viewstate/waits.go b/modules/mesh-delivery-view/viewstate/waits.go index eaee926c..07318214 100644 --- a/modules/mesh-delivery-view/viewstate/waits.go +++ b/modules/mesh-delivery-view/viewstate/waits.go @@ -15,9 +15,6 @@ import ( // `wait-ended`, and read by view_tiers through the view's own read tool. const WaitsState = "waits" -// WaitsBucket is the bus's bucket of the state waits. -const WaitsBucket = ViewModule + "_waits" - // Wait is the value of a waits key: the fields of the seat mesh-wants' wait (§498, §512, §629) as the part that holds it said // them. The view writes no words of its own: Words is the sayer's. type Wait struct {