View: no go/delivery left; wants, haves, presence and stuck through their clients, the gate's, the catalog's and the assigner's seats, the builder's builds, the build step, the viewstate; the holders and buckets it names are its own constants checked against its manifest (D-11; ADR 0634 §1, §2; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:28:52 +01:00
parent 4382ac1fcb
commit 3456f34fa8
29 changed files with 1035 additions and 776 deletions
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"mesh-delivery-view/internal/view"
"os"
"path/filepath"
"strings"
@@ -12,19 +13,25 @@ import (
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"
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
func serve(t *testing.T) string {
t.Helper()
m := view.New()
placed, err := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "x"}}})
placed, err := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "x"}}})
if err != nil {
t.Fatal(err)
}
m.Apply(view.Entry{Bucket: delivery.PlacementBucket, Key: "placement.a", Value: placed})
m.Apply(view.Entry{Bucket: view.PlacementBucket, Key: "placement.a", Value: placed})
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
if err != nil {
t.Fatal(err)
@@ -74,17 +81,17 @@ func TestFailsLoudWhenTheViewIsNotThere(t *testing.T) { // 1134/AC6
func TestPrintsTheCandidateGroupsAndTheFingerprintComparison(t *testing.T) { // 1241/AC6
m := view.New()
const head = "0123456789abcdef0123456789abcdef01234567"
cand, err := delivery.EncodeCandidate(delivery.Candidate{Group: "feat/x", Pulls: []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head}, Modules: []string{"a"}}},
HeadsHash: mustHash(t, delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head})})
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})})
if err != nil {
t.Fatal(err)
}
mod, err := delivery.EncodeModule(delivery.Module{None: "no tree", Repository: "novox/mesh-delivery", Commit: head})
mod, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{None: "no tree", Repository: "novox/mesh-delivery", Commit: head})
if err != nil {
t.Fatal(err)
}
m.Apply(view.Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Value: cand})
m.Apply(view.Entry{Bucket: delivery.ModulesBucket, Key: "a", Value: mod})
m.Apply(view.Entry{Bucket: view.CandidatesBucket, Key: "feat/x", Value: cand})
m.Apply(view.Entry{Bucket: view.ModulesBucket, Key: "a", Value: mod})
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
if err != nil {
t.Fatal(err)
@@ -108,9 +115,9 @@ func TestPrintsTheCandidateGroupsAndTheFingerprintComparison(t *testing.T) { //
}
}
func mustHash(t *testing.T, h delivery.Head) string {
func mustHash(t *testing.T, h meshdeliveryclient.Head) string {
t.Helper()
s, err := delivery.HeadsHash([]delivery.Head{h})
s, err := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{h})
if err != nil {
t.Fatal(err)
}
@@ -121,19 +128,19 @@ func mustHash(t *testing.T, h delivery.Head) string {
// 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()
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
key, err := delivery.MergeAskKey("feat/x", heads)
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
key, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
if err != nil {
t.Fatal(err)
}
ask := oc.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, ask)})
value, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, ask)})
if err != nil {
t.Fatal(err)
}
m.Apply(view.Entry{Bucket: delivery.AsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: view.AsksBucket, Key: key, Value: value})
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
if err != nil {
t.Fatal(err)
@@ -198,35 +205,35 @@ func serveWith(t *testing.T, m *view.Model) string {
// ask is refused with the phone as the reason (1297/B9b).
func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b AC
const fp = "src2:2222222222222222222222222222222222222222222222222222222222222222"
key, err := delivery.HoldAskKey("plex", fp)
key, err := asks.HoldAskKey("plex", fp)
if err != nil {
t.Fatal(err)
}
hold := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held",
hold := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "plex is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held",
Options: []oc.Option{{ID: "release", Label: "Release", Does: "goes on", Level: occ.Approve}, {ID: "keep", Label: "Keep", Does: "stays", Level: occ.Approve}}}
value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, hold)})
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
mkey, err := delivery.MergeAskKey("feat/x", heads)
value, _ := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, hold)})
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
mkey, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
if err != nil {
t.Fatal(err)
}
merge := oc.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
merge := oc.Ask{ID: meshdeliveryclient.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, merge)})
mv, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, merge)})
if err != nil {
t.Fatal(err)
}
m, w := view.New(), &putter{puts: map[string][]byte{}}
m.SetWriter(w)
m.Apply(view.Entry{Bucket: delivery.AssignerAsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: delivery.AsksBucket, Key: mkey, Value: mv})
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: view.AsksBucket, Key: mkey, Value: mv})
socket := serveWith(t, m)
var out, errOut bytes.Buffer
if code := run([]string{"answer", key, "release", "the", "check", "was", "a", "flake"}, socket, &out, &errOut); code != 0 {
t.Fatalf("exit %d: %s", code, errOut.String())
}
a, err := delivery.DecodeViewAnswer(w.puts[key+".view"])
a, err := viewstate.DecodeViewAnswer(w.puts[key+".view"])
if err != nil || a.Option != "release" || a.Words != "the check was a flake" || !strings.Contains(out.String(), key) {
t.Errorf("answer = %+v, %v; out %q", a, err, out.String())
}
@@ -250,21 +257,21 @@ func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b
// the hold ask through the view, also one answered already.
func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) {
fp := "src2:04efd1a1aa" + strings.Repeat("2", 54)
key, err := delivery.HoldAskKey("systemd-resolved", fp)
key, err := asks.HoldAskKey("systemd-resolved", fp)
if err != nil {
t.Fatal(err)
}
hold := oc.Ask{ID: delivery.AskID(key), Headline: "systemd-resolved is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held",
hold := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "systemd-resolved is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held",
Options: []oc.Option{{ID: "keep", Label: "Keep", Does: "stays", Level: occ.Acknowledge}, {ID: "release", Label: "Release", Does: "goes on", Level: occ.Acknowledge}}}
value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, hold)})
hkey, _ := delivery.HeldKey("systemd-resolved", fp)
held, _ := delivery.EncodeHeld(delivery.Held{Node: "ace", Words: "health failed, phase running: dns timeout"})
ans, _ := delivery.EncodeAnswer(delivery.Answer{Option: "keep", Warrant: hold.ID + "@2026-10-25T04:00:00Z", Words: "the operator chose keep"})
value, _ := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, hold)})
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.SetWriter(w)
m.Apply(view.Entry{Bucket: delivery.AssignerAsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: delivery.AssignerWantsBucket, Key: hkey, Value: held})
m.Apply(view.Entry{Bucket: delivery.AnswersBucket, Key: key, Value: ans})
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
m.Apply(view.Entry{Bucket: view.AssignerWantsBucket, Key: hkey, Value: held})
m.Apply(view.Entry{Bucket: view.AnswersBucket, Key: key, Value: ans})
socket := serveWith(t, m)
var out, errOut bytes.Buffer
@@ -280,7 +287,7 @@ func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) {
if code := run([]string{"hold-release", "systemd-resolved", "04efd1a1", "a", "flake"}, socket, &out, &errOut); code != 0 {
t.Fatalf("exit %d: %s", code, errOut.String())
}
if a, err := delivery.DecodeViewAnswer(w.puts[key+".view"]); err != nil || a.Option != "release" || a.Asking != hold.ID || a.Words != "a flake" {
if a, err := viewstate.DecodeViewAnswer(w.puts[key+".view"]); err != nil || a.Option != "release" || a.Asking != hold.ID || a.Words != "a flake" {
t.Errorf("the view's answer = %+v, %v", a, err)
}
errOut.Reset()
@@ -298,3 +305,12 @@ func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
}
return b
}
// askHeads is the heads of a merge ask as the seat carries them.
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
for i, h := range heads {
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
}
return out
}
@@ -6,23 +6,23 @@ package main
import (
"encoding/json"
"fmt"
"mesh-delivery-view/internal/view"
"os"
"strings"
"time"
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/busretry"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/views"
)
// ManifestReadsTool lists, per node, what its engine states it reads, by asking the node-engine seat's verb of every node
@@ -59,13 +59,13 @@ func manifestReads(r view.Reader, call enginereads.Caller) (any, error) {
return nil, fmt.Errorf("the haves cannot be read: %w", err)
}
for _, key := range keys {
node, _, isNode, err := delivery.ParseHaveKey(key)
node, _, isNode, err := nodeengineclient.ParseHaveKey(key)
if err != nil || !isNode {
continue
}
set[node] = true
if raw, err := r.HavesState.Get(key); err == nil && raw != nil {
if h, err := delivery.DecodeHaveNode(raw); err == nil && h.Record != nil {
if h, err := nodeengineclient.DecodeHaveNode(raw); err == nil && h.Record != nil {
records[node] = h.Record.ManifestReads
}
}
@@ -74,7 +74,7 @@ func manifestReads(r view.Reader, call enginereads.Caller) (any, error) {
if r.PresenceState.Keys != nil {
if keys, err := r.PresenceState.Keys(); err == nil {
for _, key := range keys {
if node, part, _, err := delivery.ParsePresenceKey(key); err == nil && part == "" && node != "" {
if node, part, _, err := meshwatchdogsclient.ParsePresenceKey(key); err == nil && part == "" && node != "" {
set[node] = true
}
}
@@ -206,7 +206,7 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
"predefined.json holds it with the merge it was read at; a merge older than that never replaces it. mesh-config " +
"publishes the predefined keys from it after its take-over. Read-only.",
Run: func(map[string]any) (any, error) {
return view.ControlRecord(r.PredefinedState, delivery.IndexerPredefinedState, delivery.PredefinedListKey)
return view.ControlRecord(r.PredefinedState, view.IndexerPredefinedState, view.PredefinedListKey)
}},
{Name: view.ConfigCandidatesReadTool,
Description: "Each open pull request's config declarations as the indexer keeps them (novox/hq ADR 0565 §2): per pull " +
@@ -216,7 +216,7 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
"key": map[string]any{"type": "string", "description": "one pull request's key, owner.repository.number"}}},
Run: func(args map[string]any) (any, error) {
key, _ := args["key"].(string)
return view.ControlRecord(r.ConfigCandidatesState, delivery.IndexerConfigCandidatesState, key)
return view.ControlRecord(r.ConfigCandidatesState, view.IndexerConfigCandidatesState, key)
}},
{Name: view.PackagesReadTool,
Description: "The mesh packages as the indexer publishes them (novox/hq ADR 0593 §2): per mesh Go module (its module " +
@@ -230,11 +230,11 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool {
key := ""
if module, _ := args["module"].(string); strings.TrimSpace(module) != "" {
var err error
if key, err = delivery.PackagesKey(strings.TrimSpace(module)); err != nil {
if key, err = view.PackagesKey(strings.TrimSpace(module)); err != nil {
return nil, err
}
}
return view.ControlRecord(r.PackagesState, delivery.PackagesState, key)
return view.ControlRecord(r.PackagesState, view.PackagesState, key)
}},
{Name: view.CandidatesReadTool,
Description: "Every candidate the indexer holds, by group, each as stored under candidates.<group>; with module, only those with a " +
@@ -395,8 +395,8 @@ func read(state stdio.KeptState) view.StateRead {
// owners), never their buckets.
func ownersRead(ask func(key string, body any) (json.RawMessage, error)) view.Reader {
// Each owner's read tool as the view's invokes names it.
indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, delivery.IndexerModule+"."+tool) }
assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, delivery.AssignerModule+"."+tool) }
indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, view.IndexerModule+"."+tool) }
assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, view.AssignerModule+"."+tool) }
return view.Reader{State: indexer("indexer_read_modules"), CandidatesState: indexer("indexer_read_candidates"),
WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"),
HeardState: indexer("indexer_read_heard"),
@@ -413,20 +413,20 @@ func main() {
r.SignersState = read(stdio.State("mesh-vault.signers"))
r.ConditionsState, r.AttemptsState = read(stdio.State(view.StatusConditionsState)), read(stdio.State(view.StatusAttemptsState))
r.PresenceState, r.StuckState = read(stdio.State(view.StatusPresenceState)), read(stdio.State(view.StatusStuckState))
r.SourceState = read(stdio.State(delivery.GateModule + ".source"))
r.SourceState = read(stdio.State(view.GateModule + ".source"))
r.ProvisionsState = read(stdio.State(provisions.ProvisionsRead))
r.BuildLog = stdio.Log(delivery.BuildLogRead).Since
r.BuildLog = stdio.Log(builds.BuildLogRead).Since
r.Pending = pendingOf
u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")),
Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())}
for _, lang := range delivery.AskLanguages {
module, _ := delivery.BuildModuleOf(lang)
u.Steps[lang] = read(stdio.State(module + "." + delivery.StepsState))
u.Pauses[lang] = read(stdio.State(module + "." + delivery.PausesState))
for _, lang := range buildstep.AskLanguages {
module, _ := buildstep.BuildModuleOf(lang)
u.Steps[lang] = read(stdio.State(module + "." + view.StepsState))
u.Pauses[lang] = read(stdio.State(module + "." + buildstep.PausesState))
}
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
// the assigner's states (ADR 0615 §6), and a re-read by hand.
tools := append(Tools(r, u), views.StatusTools(delivery.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
tools := append(Tools(r, u), views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
os.Exit(1)
@@ -435,7 +435,7 @@ func main() {
// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5).
func pendingOf() (takein.PendingAnswer, error) {
raw, err := stdio.Ask(delivery.IndexerModule+".indexer_pending", map[string]any{})
raw, err := stdio.Ask(view.IndexerModule+".indexer_pending", map[string]any{})
if err != nil {
return takein.PendingAnswer{}, err
}
@@ -4,20 +4,22 @@ import (
"crypto/ed25519"
"encoding/json"
"fmt"
"mesh-delivery-view/internal/view"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/busretry"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-sdk/go/keys"
)
type fake map[string]string
@@ -203,24 +205,24 @@ func TestBuilderBuildsListsKeysAsStored(t *testing.T) {
// the start of a later build of the same fingerprint not counted, and narrows by module through the log's key.
t0 := time.Date(2026, 10, 29, 22, 0, 0, 0, time.UTC)
entry := func(event string, at time.Duration) []byte {
e := delivery.BuildLogEntry{Event: event, Module: "netcheck", FP: fp2A, Repository: "novox/mesh-catalog", Directory: "modules/netcheck",
e := builds.BuildLogEntry{Event: event, Module: "netcheck", FP: fp2A, Repository: "novox/mesh-catalog", Directory: "modules/netcheck",
Commit: strings.Repeat("7", 40), Node: "ace", At: t0.Add(at)}
if event == delivery.BuildEndedEvent {
e.Outcome = delivery.BuildDone
if event == builds.BuildEndedEvent {
e.Outcome = builds.BuildDone
}
raw, _ := delivery.EncodeBuildLogEntry(e)
raw, _ := builds.EncodeBuildLogEntry(e)
return raw
}
var asked string
r.BuildLog = func(at time.Time, keys string, limit int) (*stdio.LogPage, error) {
asked = keys
return &stdio.LogPage{Entries: []stdio.LogEntry{{Sequence: 1, Value: entry(delivery.BuildStartedEvent, 0)},
{Sequence: 2, Value: entry(delivery.BuildEndedEvent, 90*time.Second)}, {Sequence: 3, Value: entry(delivery.BuildStartedEvent, time.Hour)},
return &stdio.LogPage{Entries: []stdio.LogEntry{{Sequence: 1, Value: entry(builds.BuildStartedEvent, 0)},
{Sequence: 2, Value: entry(builds.BuildEndedEvent, 90*time.Second)}, {Sequence: 3, Value: entry(builds.BuildStartedEvent, time.Hour)},
// an entry of another kind (the old reference form's close, design 51), of a form no build has: passed over
{Sequence: 4, Value: []byte(`{"v":99,"event":"old-form-closed","at":"2026-10-29T23:00:00Z","nodes":["ace"]}`)}}}, nil
}
out, err = run(t, r, view.BuildHistoryTool, map[string]any{"module": "netcheck", "since": t0.Format(time.RFC3339)})
if h, _ := out.(view.HistoryAnswer); err != nil || len(h.Builds) != 1 || h.Builds[0].Seconds != 90 || h.Builds[0].Outcome != delivery.BuildDone || asked != "netcheck" || h.More {
if h, _ := out.(view.HistoryAnswer); err != nil || len(h.Builds) != 1 || h.Builds[0].Seconds != 90 || h.Builds[0].Outcome != builds.BuildDone || asked != "netcheck" || h.More {
t.Errorf("history = %+v, %v (keys %q), want the one end at 90 s", out, err, asked)
}
}
@@ -491,10 +493,10 @@ func TestAssignerDifferencesAndNodeHavesToolsAnswerAsStored(t *testing.T) {
// L3 s6b (ADR 0550 §2): node_trusts answers for every node in one call whether its record names the vault's key the
// vault publishes, and every is true only once all do.
vaultPub, _, _ := ed25519.GenerateKey(nil)
vaultKey, _ := json.Marshal(delivery.NewSignerKey(vaultPub, time.Now()))
vaultFP := delivery.SignerFingerprint(vaultPub)
vaultKey, _ := json.Marshal(meshvaultclient.NewSignerKey(vaultPub, time.Now()))
vaultFP := meshvaultclient.SignerFingerprint(vaultPub)
record := func(node string, trusts ...string) string {
raw, _ := delivery.EncodeHaveNode(delivery.HaveNode{V: delivery.Version, Record: &delivery.NodeRecord{Node: node, Trusts: trusts}})
raw, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{V: keys.Version, Record: &nodeengine.NodeRecord{Node: node, Trusts: trusts}})
return string(raw)
}
for _, row := range []struct {
@@ -561,7 +563,7 @@ func TestAssignerDifferencesAndNodeHavesToolsAnswerAsStored(t *testing.T) {
// ADR 0593 §2 (D2): indexer_packages answers the indexer's published packages, one Go module by its path or all.
func packagesToolAnswersThePublishedModules(t *testing.T) {
key, err := delivery.PackagesKey("git.novox.be/novox/mesh-sdk/go")
key, err := view.PackagesKey("git.novox.be/novox/mesh-sdk/go")
if err != nil {
t.Fatal(err)
}
@@ -601,10 +603,10 @@ func manifestReadsAsksEveryNode(t *testing.T) {
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
}
record := func(node string, reads ...string) string {
raw, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, ManifestReads: reads}})
raw, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: node, ManifestReads: reads}})
return string(raw)
}
on, _ := delivery.EncodePresence(delivery.Presence{Online: true})
on, _ := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: true})
r := view.Reader{HavesState: fake{"ace.node": record("ace"), "g14.node": record("g14", "word:module"), "ace.web": "{}"}.reader().State,
PresenceState: fake{"novox": string(on)}.reader().State}
started := time.Now()
@@ -3,17 +3,14 @@ package main
import (
"encoding/json"
"fmt"
"mesh-delivery-view/internal/view"
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-sdk/go/views"
)
// source is a fake owner state as a views.Source: what the owner's read tool answers from.
@@ -49,45 +46,45 @@ func (k kept) History(key string) ([]views.Revision, error) { return k.history[k
// the owners' read tools (the real owner side, views.Read, over a fake state) what it answered reading the states.
func ownersAnswerTheToolsAsBefore(t *testing.T) {
cand := `{"v":1,"group":"feat/x","base":3,"heads":"h","pulls":[{"modules":["zsh"],"fps":{"zsh":{"fp2":"` + fp2B + `","manifest":"m"}}}]}`
pkgKey, err := delivery.PackagesKey("git.novox.be/novox/mesh-sdk/go")
pkgKey, err := view.PackagesKey("git.novox.be/novox/mesh-sdk/go")
if err != nil {
t.Fatal(err)
}
states := map[string]source{
delivery.IndexerModule + ".modules": {"a": withFP2, "b": withNone},
delivery.IndexerModule + ".candidates": {"feat/x": cand, "fix": `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`},
delivery.AssignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`,
view.IndexerModule + ".modules": {"a": withFP2, "b": withNone},
view.IndexerModule + ".candidates": {"feat/x": cand, "fix": `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`},
view.AssignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`,
"held." + keyB: `{"node":"ace","words":"failed"}`},
delivery.AssignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`},
delivery.IndexerModule + "." + delivery.IndexerPredefinedState: {delivery.PredefinedListKey: `{"v":1,"keys":["dir:state"]}`},
delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState: {"novox.r.7": `{"v":1,"pull":"novox/r#7"}`},
delivery.IndexerModule + "." + delivery.PackagesState: {pkgKey: `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}`},
delivery.IndexerModule + "." + graph.State: {},
view.AssignerModule + ".differences": {"ace.zsh": `{"unexplained":true}`, "novox.zsh": `{"rule":3}`},
view.IndexerModule + "." + view.IndexerPredefinedState: {view.PredefinedListKey: `{"v":1,"keys":["dir:state"]}`},
view.IndexerModule + "." + view.IndexerConfigCandidatesState: {"novox.r.7": `{"v":1,"pull":"novox/r#7"}`},
view.IndexerModule + "." + view.PackagesState: {pkgKey: `{"v":1,"module":"git.novox.be/novox/mesh-sdk/go","commit":"c1"}`},
view.IndexerModule + "." + graph.State: {},
}
// The kept history of the indexer's modules and heard and the assigner's wants (each declared with more than one
// revision in its owner's manifest), which indexer_module_history, indexer_heard and view_tiers' times read.
at := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
const heard1 = `{"v":1,"subject":"s","sequence":41,"repository":"novox/mesh-catalog","commit":"9cdac09a"}`
states[delivery.IndexerModule+".heard"] = source{"forge": heard1}
states[view.IndexerModule+".heard"] = source{"forge": heard1}
histories := map[string]map[string][]views.Revision{
delivery.IndexerModule + ".modules": {"a": {{Revision: 7, At: at, Op: "put", Value: json.RawMessage(withNone)},
view.IndexerModule + ".modules": {"a": {{Revision: 7, At: at, Op: "put", Value: json.RawMessage(withNone)},
{Revision: 9, At: at.Add(time.Minute), Op: "put", Value: json.RawMessage(withFP2)}}},
delivery.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}},
delivery.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}},
view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}},
view.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}},
}
// The direct reading, as the tools read the states before (the reads the manifest no longer has).
st := func(name string) view.StateRead { return fake(states[name]).reader().State }
direct := view.Reader{State: st(delivery.IndexerModule + ".modules"), CandidatesState: st(delivery.IndexerModule + ".candidates"),
WantsState: st(delivery.AssignerModule + ".wants"), DifferencesState: st(delivery.AssignerModule + ".differences"),
PredefinedState: st(delivery.IndexerModule + "." + delivery.IndexerPredefinedState),
ConfigCandidatesState: st(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState),
PackagesState: st(delivery.IndexerModule + "." + delivery.PackagesState),
GraphState: st(delivery.IndexerModule + "." + graph.State)}
direct := view.Reader{State: st(view.IndexerModule + ".modules"), CandidatesState: st(view.IndexerModule + ".candidates"),
WantsState: st(view.AssignerModule + ".wants"), DifferencesState: st(view.AssignerModule + ".differences"),
PredefinedState: st(view.IndexerModule + "." + view.IndexerPredefinedState),
ConfigCandidatesState: st(view.IndexerModule + "." + view.IndexerConfigCandidatesState),
PackagesState: st(view.IndexerModule + "." + view.PackagesState),
GraphState: st(view.IndexerModule + "." + graph.State)}
// The owners' read tools, answered by the owner's own Read over the same state; any other address is refused.
tools := map[string]string{}
for name := range states {
owner, state, _ := strings.Cut(name, ".")
short := map[string]string{delivery.IndexerModule: "indexer_read_", delivery.AssignerModule: "assigner_read_"}[owner]
short := map[string]string{view.IndexerModule: "indexer_read_", view.AssignerModule: "assigner_read_"}[owner]
tools[owner+"."+short+strings.ReplaceAll(state, "-", "_")] = name
}
asked := map[string]int{}
@@ -161,7 +158,7 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
}
// Each owner state the tools read was asked of its owner's read tool.
for tool, name := range tools {
if name != delivery.IndexerModule+"."+graph.State && asked[tool] == 0 {
if name != view.IndexerModule+"."+graph.State && asked[tool] == 0 {
t.Errorf("%s was never asked", tool)
}
}
@@ -4,18 +4,22 @@ import (
"encoding/json"
"errors"
"fmt"
"mesh-delivery-view/internal/view"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-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"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
"mesh-delivery-view/internal/view"
)
// encoded is v as its contract's encoder writes it, failing the test when it refuses.
@@ -37,23 +41,23 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
manifest := strings.Repeat("d", 64)
mod := func(f string, src uint64) string {
b, err := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: src, Repository: "novox/mesh-catalog", Commit: "c"})
b, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: f, Manifest: manifest, Src: int64(src), FpSrc: int64(src), Repository: "novox/mesh-catalog", Commit: "c"})
return encoded(t, b, err)
}
node := func(g graph.Node) string {
b, err := graph.Encode(g)
return encoded(t, b, err)
}
build := func(b delivery.BuilderBuild) string {
v, err := delivery.EncodeBuilderBuild(b)
build := func(b builds.BuilderBuild) string {
v, err := builds.EncodeBuilderBuild(b)
return encoded(t, v, err)
}
want := func(w delivery.Want) string {
v, err := delivery.EncodeWant(w)
want := func(w meshwantsclient.Want) string {
v, err := meshwantsclient.EncodeWant(w)
return encoded(t, v, err)
}
have := func(h delivery.Have) string {
v, err := delivery.EncodeHave(h)
have := func(h nodeengine.HaveByNodeModuleValue) string {
v, err := nodeengineclient.EncodeHave(h)
return encoded(t, v, err)
}
key := func(k string, err error) string {
@@ -70,22 +74,22 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
"lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}),
}.reader().State
r.Builds = fake{
key(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("web", fp("b"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}),
key(delivery.BuilderBuildKey("lib", fp("c"))): build(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "@novox/x@1.2.0"}),
key(builds.BuilderBuildKey("db", fp("a"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
key(builds.BuilderBuildKey("web", fp("b"))): build(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifest}),
key(builds.BuilderBuildKey("lib", fp("c"))): build(builds.BuilderBuild{State: builds.BuildQueued, Waits: "@novox/x@1.2.0"}),
}.reader().State
r.WantsState = fake{
key(delivery.WantKey("ace", "db")): want(delivery.Want{FP: fp("a"), Self: true}),
key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
key(delivery.WantKey("g14", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleCanary, Words: "the canary"}),
key(meshwantsclient.WantKey("ace", "db")): want(meshwantsclient.Want{Fp: fp("a"), Self: true}),
key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}),
key(meshwantsclient.WantKey("g14", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleCanary, Words: "the canary"}),
}.reader().State
r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil }
r.HavesState = fake{key(delivery.HaveKey("ace", "db")): have(delivery.Have{FP: fp("a"), Phase: delivery.PhaseRunning, Health: delivery.HealthOK})}.reader().State
r.HavesState = fake{key(nodeengineclient.HaveKey("ace", "db")): have(nodeengine.HaveByNodeModuleValue{Fp: fp("a"), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})}.reader().State
p, err := provisions.Encode(provisions.Provision{Provision: "database", Consumer: provisions.End{Module: "web", Node: "ace"},
Provider: provisions.End{Module: "db", Node: "ace"}, State: provisions.Asked, Why: "the build db runs does not offer it", At: since})
r.ProvisionsState = fake{key(provisions.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State
off, err := delivery.EncodePresence(delivery.Presence{Online: false})
r.PresenceState = fake{key(delivery.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State
off, err := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: false})
r.PresenceState = fake{key(meshwatchdogsclient.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
@@ -99,7 +103,7 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
}
web := got.Tiers[1][0]
if web.Module != "web" || !reflect.DeepEqual(web.After, []string{"db"}) || len(web.Waits) != 2 ||
web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != delivery.RuleProvisions || !web.Waits[0].Since.Equal(since) ||
web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != meshwantsclient.RuleProvisions || !web.Waits[0].Since.Equal(since) ||
web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") {
t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web)
}
@@ -152,7 +156,7 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
graph.ReaderWaitWords("ace", []string{"tool-runner"}, "web", short(fp("b"))): "reader capability tool-runner",
"the provision ace.web.database of web on ace is asked, not provided": "want",
} {
r.WantsState = fake{key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: words})}.reader().State
r.WantsState = fake{key(meshwantsclient.WantKey("ace", "web")): want(meshwantsclient.Want{Self: true, Fp: fp("f"), Newest: fp("b"), Rule: meshwantsclient.RuleProvisions, Words: words})}.reader().State
out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
got, _ := out.(view.Tiers)
if err != nil || len(got.Tiers) != 2 {
@@ -6,24 +6,33 @@ import (
"context"
"encoding/json"
"io"
"mesh-delivery-view/internal/view"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
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"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-view/internal/view"
)
// serveOwners serves the six states of the indexer and the assigner the view's process follows by their first views
@@ -32,7 +41,7 @@ import (
func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
t.Helper()
for _, o := range ownersFollowed {
viewstest.Serve(t, ctx, js, nc, o.owner, o.state, o.tool, "n_"+delivery.ViewModule)
viewstest.Serve(t, ctx, js, nc, o.owner, o.state, o.tool, "n_"+viewstate.ViewModule)
}
}
@@ -61,7 +70,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil {
t.Fatal(err)
@@ -74,8 +83,8 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
t.Fatal(err)
}
}
haveValue := func(build string, phase delivery.Phase, health delivery.Health) []byte {
b, err := delivery.EncodeHave(delivery.Have{Build: build, Phase: phase, Health: health})
haveValue := func(build string, phase string, health string) []byte {
b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: build, Phase: phase, Health: health})
if err != nil {
t.Fatal(err)
}
@@ -83,20 +92,20 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
}
// The seat's placement (issue 774, ADR 0549 §3): x and y on a, nothing on b.
placeValue := func(node string, modules ...string) []byte {
p := delivery.Placement{Node: node}
p := meshmoduleassigner.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
v, err := delivery.EncodePlacement(p)
v, err := meshmoduleassignerclient.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
return v
}
put(delivery.PlacementBucket, "placement.a", placeValue("a", "x", "y"))
put(delivery.PlacementBucket, "placement.b", placeValue("b"))
put(delivery.HaveBucket, "a.x", haveValue("b1", delivery.PhaseChecking, delivery.HealthWaits))
put(delivery.HaveBucket, "b.x", haveValue("b1", delivery.PhaseRunning, delivery.HealthOK))
put(view.PlacementBucket, "placement.a", placeValue("a", "x", "y"))
put(view.PlacementBucket, "placement.b", placeValue("b"))
put(view.HaveBucket, "a.x", haveValue("b1", nodeengineclient.PhaseChecking, nodeengineclient.HealthWaits))
put(view.HaveBucket, "b.x", haveValue("b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK))
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "i")
if err != nil {
@@ -145,7 +154,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8
t.Fatalf("counted = %d, want 2 (a.x, a.y): %+v", s.Counted(), s.Rows)
}
put(delivery.HaveBucket, "a.x", haveValue("b1", delivery.PhaseRunning, delivery.HealthOK))
put(view.HaveBucket, "a.x", haveValue("b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK))
rowsMatch(func(s view.Snapshot) bool { r := byKey(s, "a", "x"); return r.Phase == "running" && r.Health == "ok" })
}
@@ -173,7 +182,7 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5})
if err != nil {
t.Fatal(err)
@@ -182,15 +191,15 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5
}
presence := func(key string, online bool) {
t.Helper()
b, _ := delivery.EncodePresence(delivery.Presence{Online: online})
if _, err := kvs[delivery.PresenceBucket].Put(ctx, key, b); err != nil {
b, _ := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: online})
if _, err := kvs[view.PresenceBucket].Put(ctx, key, b); err != nil {
t.Fatal(err)
}
}
stuck := func(moved bool) {
t.Helper()
b, _ := delivery.EncodeStuck(delivery.Stuck{Bucket: delivery.HaveBucket, Key: "a.x", Waited: "a report from a", Since: time.Now(), Moved: moved})
if _, err := kvs[delivery.StuckBucket].Put(ctx, delivery.HaveBucket+".a.x", b); err != nil {
b, _ := meshwatchdogsclient.EncodeStuck(meshwatchdogsclient.Stuck{Bucket: view.HaveBucket, Key: "a.x", Waited: "a report from a", Since: time.Now(), Moved: moved})
if _, err := kvs[view.StuckBucket].Put(ctx, view.HaveBucket+".a.x", b); err != nil {
t.Fatal(err)
}
}
@@ -260,7 +269,7 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
for _, b := range []string{delivery.HaveBucket, delivery.PlacementBucket, delivery.PresenceBucket, delivery.StuckBucket} {
for _, b := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 5}); err != nil {
t.Fatal(err)
}
@@ -289,21 +298,21 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
break
}
}
cands, err := js.KeyValue(ctx, delivery.CandidatesBucket)
cands, err := js.KeyValue(ctx, view.CandidatesBucket)
if err != nil {
t.Fatal(err)
}
mods, err := js.KeyValue(ctx, delivery.ModulesBucket)
mods, err := js.KeyValue(ctx, view.ModulesBucket)
if err != nil {
t.Fatal(err)
}
const head = "0123456789abcdef0123456789abcdef01234567"
hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: head}})
cand, err := delivery.EncodeCandidate(delivery.Candidate{Group: "feat/x", HeadsHash: hash, Pulls: []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head}, Modules: []string{"a"}}}})
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: head}})
cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{Group: "feat/x", Heads: hash, Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-delivery", Number: 12, Head: head, Modules: []string{"a"}}}})
if err != nil {
t.Fatal(err)
}
mod, err := delivery.EncodeModule(delivery.Module{None: "no tree", Repository: "novox/mesh-delivery", Commit: head})
mod, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{None: "no tree", Repository: "novox/mesh-delivery", Commit: head})
if err != nil {
t.Fatal(err)
}
@@ -370,23 +379,23 @@ func TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes(t *testing.T) { //
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
t.Cleanup(func() { stop(); <-done })
gate, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.AsksBucket, History: delivery.AsksHistory})
gate, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: view.AsksBucket, History: view.AsksHistory})
if err != nil {
t.Fatal(err)
}
teller, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.AnswersBucket, History: delivery.AnswersHistory})
teller, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: view.AnswersBucket, History: view.AnswersHistory})
if err != nil {
t.Fatal(err)
}
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
key, err := delivery.MergeAskKey("feat/x", heads)
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
key, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
if err != nil {
t.Fatal(err)
}
ask := oc.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, ask)})
value, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, ask)})
if err != nil {
t.Fatal(err)
}
@@ -409,7 +418,7 @@ func TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes(t *testing.T) { //
wait(func(s view.Snapshot) bool {
return len(s.Asks) == 1 && s.Asks[0].Key == key && s.Asks[0].How == "answer on the phone"
}, "the open ask never showed")
answer, err := delivery.EncodeAnswer(delivery.Answer{Option: "approve", Warrant: ask.ID + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
answer, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "approve", Warrant: ask.ID + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
if err != nil {
t.Fatal(err)
}
@@ -461,17 +470,17 @@ func TestProcessWholeListsEveryDifference(t *testing.T) { // 1297/B8 AC
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
t.Cleanup(func() { stop(); <-done })
kv, err := js.KeyValue(ctx, delivery.AssignerWantsBucket)
kv, err := js.KeyValue(ctx, view.AssignerWantsBucket)
if err != nil {
t.Fatal(err)
}
const fpOld = "src2:1111111111111111111111111111111111111111111111111111111111111111"
const fpNew = "src2:2222222222222222222222222222222222222222222222222222222222222222"
for key, d := range map[string]delivery.Want{
"want.ace.plex": {FP: fpNew, Composed: "sha256:" + strings.Repeat("c", 64)},
"want.g14.plex": {FP: fpOld, Composed: "sha256:" + strings.Repeat("c", 64), Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace does not run it healthy yet"},
for key, d := range map[string]meshwantsclient.Want{
"want.ace.plex": {Fp: fpNew, Composed: "sha256:" + strings.Repeat("c", 64)},
"want.g14.plex": {Fp: fpOld, Composed: "sha256:" + strings.Repeat("c", 64), Newest: fpNew, Rule: meshwantsclient.RuleCanary, Words: "the canary ace does not run it healthy yet"},
} {
v, err := delivery.EncodeWant(d)
v, err := meshwantsclient.EncodeWant(d)
if err != nil {
t.Fatal(err)
}
@@ -494,7 +503,7 @@ func TestProcessWholeListsEveryDifference(t *testing.T) { // 1297/B8 AC
}
wait(func(s view.Snapshot) bool {
d := s.Wants
return len(d) == 2 && d[0].Node == "ace" && d[0].State == view.WantPending && d[1].Node == "g14" && d[1].State == view.WantHeldBack && d[1].Rule == delivery.RuleCanary
return len(d) == 2 && d[0].Node == "ace" && d[0].State == view.WantPending && d[1].Node == "g14" && d[1].State == view.WantHeldBack && d[1].Rule == meshwantsclient.RuleCanary
}, "the two wants never showed with where they stand")
if err := kv.Delete(ctx, "want.g14.plex"); err != nil {
t.Fatal(err)
@@ -536,10 +545,10 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
}
return kv
}
askBucket := bucket(delivery.AssignerAsksBucket, delivery.AssignerAsksHistory)
viewBucket := bucket(delivery.ViewAnswersBucket, delivery.ViewAnswersHistory)
teller := bucket(delivery.AnswersBucket, delivery.AnswersHistory)
gate := bucket(delivery.AsksBucket, delivery.AsksHistory)
askBucket := bucket(view.AssignerAsksBucket, view.AssignerAsksHistory)
viewBucket := bucket(viewstate.ViewAnswersBucket, viewstate.ViewAnswersHistory)
teller := bucket(view.AnswersBucket, view.AnswersHistory)
gate := bucket(view.AsksBucket, view.AsksHistory)
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "i")
if err != nil {
@@ -560,14 +569,14 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
t.Cleanup(func() { stop(); <-done })
const fp = "src2:2222222222222222222222222222222222222222222222222222222222222222"
key, err := delivery.HoldAskKey("plex", fp)
key, err := asks.HoldAskKey("plex", fp)
if err != nil {
t.Fatal(err)
}
ask := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator,
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator,
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the fingerprint stays held",
Options: []oc.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: occ.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: occ.Approve}}}
value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)})
value, _ := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, ask)})
if _, err := askBucket.Put(ctx, key, value); err != nil {
t.Fatal(err)
}
@@ -593,20 +602,20 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
if err != nil {
t.Fatalf("the view wrote no answer under %s.view: %v", key, err)
}
if a, err := delivery.DecodeViewAnswer(e.Value()); err != nil || a.Option != "release" {
if a, err := viewstate.DecodeViewAnswer(e.Value()); err != nil || a.Option != "release" {
t.Fatalf("answer = %+v, %v", a, err)
}
wait(func(s view.Snapshot) bool { return len(s.Asks) == 1 && s.Asks[0].Chosen == "release" }, "the ask never showed the view's choice")
// The merge's Approve is answered on the phone alone.
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
mkey, err := delivery.MergeAskKey("feat/x", heads)
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
mkey, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
if err != nil {
t.Fatal(err)
}
merge := oc.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
merge := oc.Ask{ID: meshdeliveryclient.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, merge)})
mv, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, merge)})
if err != nil {
t.Fatal(err)
}
@@ -622,7 +631,7 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
}
// The teller fills the ask: it leaves the list.
answer, err := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: ask.ID + "@2026-10-23T12:00:00Z", Words: "answered in the view"})
answer, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "release", Warrant: ask.ID + "@2026-10-23T12:00:00Z", Words: "answered in the view"})
if err != nil {
t.Fatal(err)
}
@@ -656,28 +665,28 @@ func TestProcessWholeShowsTheBacklogOfBuildAsks(t *testing.T) { // 1887/A8a AC
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
stream, _ := delivery.AskStream(delivery.AskGo)
stream, _ := buildstep.AskStream(buildstep.AskGo)
st, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: stream, Subjects: []string{"mesh.seat.node-build-go.accept.>"}, Retention: jetstream.WorkQueuePolicy})
if err != nil {
t.Fatal(err)
}
var checkWorker jetstream.Consumer
for _, kind := range delivery.AskKinds {
name, _ := delivery.AskWorker(delivery.AskGo, kind)
subject, _ := delivery.AskSubject(delivery.AskGo, kind)
for _, kind := range buildstep.AskKinds {
name, _ := buildstep.AskWorker(buildstep.AskGo, kind)
subject, _ := buildstep.AskSubject(buildstep.AskGo, kind)
c, err := st.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{Durable: name, FilterSubject: subject, AckPolicy: jetstream.AckExplicitPolicy})
if err != nil {
t.Fatal(err)
}
if kind == delivery.AskCheck {
if kind == buildstep.AskCheck {
checkWorker = c
}
}
subject, _ := delivery.AskSubject(delivery.AskGo, delivery.AskCheck)
subject, _ := buildstep.AskSubject(buildstep.AskGo, buildstep.AskCheck)
oldest := time.Now().UTC().Add(-2 * time.Minute)
for i, id := range []string{"m0000001aaaaaa", "m0000002bbbbbb", "m0000003cccccc", "m0000004dddddd"} {
body, err := delivery.EncodeStepAsk(delivery.StepAsk{ID: id, Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "mesh-delivery-checker", At: oldest.Add(time.Duration(i) * time.Second),
Check: &delivery.CheckJob{Fetch: map[string]any{"repository": "novox/x", "commit": "0123456789abcdef0123456789abcdef01234567"}, Levels: []string{delivery.LevelUnit}}})
body, err := buildstep.EncodeStepAsk(buildstep.StepAsk{ID: id, Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: "mesh-delivery-checker", At: oldest.Add(time.Duration(i) * time.Second),
Check: &buildstep.CheckJob{Fetch: buildstep.Fetch{Repository: "novox/x", Commit: "0123456789abcdef0123456789abcdef01234567"}, Levels: []string{buildstep.LevelUnit}}})
if err != nil {
t.Fatal(err)
}
@@ -755,7 +764,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) {
t.Fatal(err)
}
kvs := map[string]jetstream.KeyValue{}
for _, name := range []string{delivery.SourceBucket, delivery.ModulesBucket, delivery.AssignerWantsBucket, delivery.HaveBucket} {
for _, name := range []string{view.SourceBucket, view.ModulesBucket, view.AssignerWantsBucket, view.HaveBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 5})
if err != nil {
t.Fatal(err)
@@ -780,23 +789,23 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) {
t.Cleanup(func() { stop(); <-done })
fp := "src2:" + strings.Repeat("2", 64)
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: strings.Repeat("a", 64), Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: delivery.RootPath})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: "sha256:" + strings.Repeat("c", 64)})
failed, _ := delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: delivery.HealthFailed})
good, _ := delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
src, _ := delivery.EncodeSource(delivery.Source{Group: "feat/x", Heads: map[string]string{"novox/web": strings.Repeat("1", 40)}})
for bucket, kv := range map[string][]byte{delivery.ModulesBucket: mod} {
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: strings.Repeat("a", 64), Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath})
want, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fp, Composed: "sha256:" + strings.Repeat("c", 64)})
failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed})
good, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
src, _ := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "feat/x", Heads: map[string]string{"novox/web": strings.Repeat("1", 40)}})
for bucket, kv := range map[string][]byte{view.ModulesBucket: mod} {
if _, err := kvs[bucket].Put(ctx, "web", kv); err != nil {
t.Fatal(err)
}
}
if _, err := kvs[delivery.AssignerWantsBucket].Put(ctx, "want.ace.web", want); err != nil {
if _, err := kvs[view.AssignerWantsBucket].Put(ctx, "want.ace.web", want); err != nil {
t.Fatal(err)
}
if _, err := kvs[delivery.HaveBucket].Put(ctx, "ace.web", failed); err != nil {
if _, err := kvs[view.HaveBucket].Put(ctx, "ace.web", failed); err != nil {
t.Fatal(err)
}
rev, err := kvs[delivery.SourceBucket].Put(ctx, delivery.SourceMainKey, src)
rev, err := kvs[view.SourceBucket].Put(ctx, meshdeliveryclient.SourceMainKey, src)
if err != nil {
t.Fatal(err)
}
@@ -804,7 +813,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) {
if info, err := events.Info(ctx, jetstream.WithSubjectFilter("mesh.mod.mesh-delivery-view.event.>")); err != nil || len(info.State.Subjects) != 0 {
t.Fatalf("an event was said before the revision was delivered: %v %+v", err, info.State)
}
if _, err := kvs[delivery.HaveBucket].Put(ctx, "ace.web", good); err != nil {
if _, err := kvs[view.HaveBucket].Put(ctx, "ace.web", good); err != nil {
t.Fatal(err)
}
var msg *jetstream.RawStreamMsg
@@ -852,7 +861,7 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
t.Fatal(err)
}
kvs := map[string]jetstream.KeyValue{}
for _, name := range []string{delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.StuckBucket} {
for _, name := range []string{view.AssignerWantsBucket, view.HaveBucket, view.StuckBucket} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 5})
if err != nil {
t.Fatal(err)
@@ -889,16 +898,16 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
}
}
want := func(node, module string) {
v, err := delivery.EncodeWant(delivery.Want{FP: fpNew, Composed: digest})
put(delivery.AssignerWantsBucket, "want."+node+"."+module, v, err)
v, err := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fpNew, Composed: digest})
put(view.AssignerWantsBucket, "want."+node+"."+module, v, err)
}
have := func(node, module, fp string, phase delivery.Phase, health delivery.Health, detail string) {
v, err := delivery.EncodeHave(delivery.Have{FP: fp, Phase: phase, Health: health, Detail: detail})
put(delivery.HaveBucket, node+"."+module, v, err)
have := func(node, module, fp string, phase string, health string, detail string) {
v, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: phase, Health: health, Detail: detail})
put(view.HaveBucket, node+"."+module, v, err)
}
have("ace", "web", fpOld, delivery.PhaseRunning, delivery.HealthOK, "")
have("ace", "web", fpOld, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, "")
want("ace", "web")
have("g14", "web", fpOld, delivery.PhaseRunning, delivery.HealthOK, "")
have("g14", "web", fpOld, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, "")
want("g14", "web")
said := func() map[string]jetstream.RawStreamMsg {
@@ -936,12 +945,12 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
if mv.Subject != "mesh.mod.mesh-delivery-view.event.moving" {
t.Errorf("subject = %s", mv.Subject)
}
have("ace", "web", fpNew, delivery.PhaseRunning, delivery.HealthOK, "")
have("ace", "web", fpNew, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, "")
st := waitFor("settled.ace.web." + hex)
if err := json.Unmarshal(st.Data, &body); err != nil || body.Outcome != "running" {
t.Errorf("settled = %+v, %v", body, err)
}
have("g14", "web", fpOld, delivery.PhaseHeld, delivery.HealthWaits, "no space left")
have("g14", "web", fpOld, nodeengineclient.PhaseHeld, nodeengineclient.HealthWaits, "no space left")
stk := waitFor("stuck.g14.web." + hex)
if err := json.Unmarshal(stk.Data, &body); err != nil || body.Outcome != "stopped" || body.Words != "no space left" {
t.Errorf("stuck = %+v, %v", body, err)
@@ -955,3 +964,12 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
t.Errorf("events said by type = %v, want moving 2, settled 1, stuck 1", count)
}
}
// askHeads is the heads of a merge ask as the seat carries them.
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
for i, h := range heads {
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
}
return out
}
@@ -11,9 +11,9 @@ import (
"encoding/json"
"errors"
"fmt"
sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings"
"io"
"log"
"mesh-delivery-view/internal/view"
"net/http"
"os"
"os/signal"
@@ -21,14 +21,14 @@ import (
"syscall"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-view/internal/view"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
const (
@@ -87,7 +87,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
model.SetDeliveredEvent(func(e view.DeliveredEvent) error {
id := fmt.Sprintf("delivered.%d", e.Revision)
if e.Repository != "" { // a repository's reached head (ADR 0556 clause 3)
key, _ := delivery.RepositoryKey(e.Repository)
key, _ := trunk.RepositoryKey(e.Repository)
id = fmt.Sprintf("reached.%s.%d.%d", key, e.Revision, len(e.NotYet))
if len(e.NotYet) > 0 {
sum := sha256.Sum256([]byte(strings.Join(e.NotYet, ",")))
@@ -117,7 +117,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer calls.Close()
reader := &views.ProcessReader{Module: delivery.ViewModule, Call: calls.Call, Logf: logger.Printf}
reader := &views.ProcessReader{Module: viewstate.ViewModule, Call: calls.Call, Logf: logger.Printf}
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) }
for _, o := range ownersFollowed {
@@ -126,12 +126,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
}
open := buckets{js: js, reader: reader}
for _, bucket := range []string{delivery.HaveBucket, delivery.PlacementBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket, delivery.TakenBucket} {
for _, bucket := range []string{view.HaveBucket, view.PlacementBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
go follow(ctx, open, bucket, false, model, logger)
}
// The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556).
// The modules' picture holds no history: from its first view on, each change it takes is the history (finding of DLV-M4).
for _, bucket := range []string{delivery.ModulesBucket, delivery.SourceBucket} {
for _, bucket := range []string{view.ModulesBucket, view.SourceBucket} {
go follow(ctx, open, bucket, true, model, logger)
}
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf))
@@ -156,9 +156,9 @@ type kvWriter struct {
}
func (w kvWriter) Put(key string, value []byte) error {
kv, err := w.js.KeyValue(w.ctx, delivery.ViewAnswersBucket)
kv, err := w.js.KeyValue(w.ctx, viewstate.ViewAnswersBucket)
if err != nil {
return fmt.Errorf("the bucket %s: %w", delivery.ViewAnswersBucket, err)
return fmt.Errorf("the bucket %s: %w", viewstate.ViewAnswersBucket, err)
}
_, err = kv.Put(w.ctx, key, value)
return err
@@ -166,9 +166,9 @@ func (w kvWriter) Put(key string, value []byte) error {
// 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) error {
kv, err := js.KeyValue(ctx, delivery.ReachBucket)
kv, err := js.KeyValue(ctx, viewstate.ReachBucket)
if err != nil {
return fmt.Errorf("the bucket %s: %w", delivery.ReachBucket, err)
return fmt.Errorf("the bucket %s: %w", viewstate.ReachBucket, err)
}
_, err = kv.Put(ctx, w.Key, w.Value)
return err
@@ -177,20 +177,20 @@ func putReach(ctx context.Context, js jetstream.JetStream, w view.ReachWrite) er
// The view's own states of its first views (views.StatusState) and of its findings, where the condition keeper raises a
// first view unanswered past its bound (ADR 0615 §6).
var (
firstViewsBucket = delivery.ViewModule + "_" + views.StatusState
findingsBucket = delivery.ViewModule + "_findings"
firstViewsBucket = viewstate.ViewModule + "_" + views.StatusState
findingsBucket = viewstate.ViewModule + "_findings"
)
// ownersFollowed are the indexer's and the assigner'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.
var ownersFollowed = []struct{ owner, state, tool string }{
{delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule + ".assigner_read_placement"},
{delivery.AssignerModule, "wants", delivery.AssignerModule + ".assigner_read_wants"},
{delivery.AssignerModule, "asks", delivery.AssignerModule + ".assigner_read_asks"},
{delivery.IndexerModule, "candidates", delivery.IndexerModule + ".indexer_read_candidates"},
{delivery.IndexerModule, "modules", delivery.IndexerModule + ".indexer_read_modules"},
{delivery.IndexerModule, "taken", delivery.IndexerModule + ".indexer_read_taken"},
{view.AssignerModule, view.PlacementState, view.AssignerModule + ".assigner_read_placement"},
{view.AssignerModule, "wants", view.AssignerModule + ".assigner_read_wants"},
{view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"},
{view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"},
{view.IndexerModule, "modules", view.IndexerModule + ".indexer_read_modules"},
{view.IndexerModule, "taken", view.IndexerModule + ".indexer_read_taken"},
}
// buckets opens what the view watches: an owner's state as its picture (firstview), any other bucket on the bus.
+13
View File
@@ -3,6 +3,19 @@ module mesh-delivery-view
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
@@ -12,7 +12,9 @@ import (
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"
"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"
)
// memWriter is the view's bucket answers in memory.
@@ -36,18 +38,18 @@ func (w *memWriter) Put(key string, value []byte) error {
// none yet; the value is `{"v":1,"ask":<the router's ask>}`, as the gate's minus group and heads).
func holdAsk(t *testing.T) (Entry, oc.Ask) {
t.Helper()
key, err := delivery.HoldAskKey("plex", fpNew)
key, err := asks.HoldAskKey("plex", fpNew)
if err != nil {
t.Fatal(err)
}
a := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check on the new build.", Who: occ.Operator,
a := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check on the new build.", Who: occ.Operator,
Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the fingerprint stays held",
Options: []oc.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: occ.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: occ.Approve}}}
v, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, a)})
v, err := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, a)})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AssignerAsksBucket, Key: key, Value: v, At: t0}, a
return Entry{Bucket: AssignerAsksBucket, Key: key, Value: v, At: t0}, a
}
func TestAnAskNeedingNoProofIsOpenInTheViewWithItsOptions(t *testing.T) { // 1297/B9
@@ -77,16 +79,16 @@ func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a A
if !ok || len(w.puts) != 1 {
t.Fatalf("puts = %v, want the one key %s.view", w.puts, e.Key)
}
a, err := delivery.DecodeViewAnswer(raw)
a, err := viewstate.DecodeViewAnswer(raw)
if err != nil || a.Option != "release" || a.Words != "the check was a flake" {
t.Errorf("answer = %+v, %v", a, err)
}
// The key is the SDK's: the bucket's key parses back to the ask.
if back, err := delivery.ParseViewAnswerKey(e.Key + ".view"); err != nil || back != e.Key {
if back, err := viewstate.ParseViewAnswerKey(e.Key + ".view"); err != nil || back != e.Key {
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: delivery.ViewAnswersBucket, Key: e.Key + ".view", Value: raw, At: t0})
fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, 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)
@@ -220,11 +222,11 @@ func TestAStaleViewAnswerOfAnotherAskingIsNotTheChoice(t *testing.T) { // 1297/B
m := New()
e, a := holdAsk(t)
fill(m, e)
stale, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: "an-earlier-asking", Option: "release"})
stale, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: "an-earlier-asking", Option: "release"})
if err != nil {
t.Fatal(err)
}
fill(m, Entry{Bucket: delivery.ViewAnswersBucket, Key: e.Key + ".view", Value: stale, At: t0})
fill(m, Entry{Bucket: viewstate.ViewAnswersBucket, 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)
}
@@ -234,7 +236,7 @@ func TestAStaleViewAnswerOfAnotherAskingIsNotTheChoice(t *testing.T) { // 1297/B
if err := m.Answer(e.Key, "keep", ""); err != nil {
t.Fatal(err)
}
if v, err := delivery.DecodeViewAnswer(w.puts[e.Key+".view"]); err != nil || v.Asking != a.ID {
if v, err := viewstate.DecodeViewAnswer(w.puts[e.Key+".view"]); err != nil || v.Asking != a.ID {
t.Errorf("answer = %+v, %v; want asking %s", v, err, a.ID)
}
}
@@ -9,9 +9,11 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
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"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
const askHeadA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
@@ -20,31 +22,33 @@ const askHeadA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// whole, under the id its key gives.
func askEntry(t *testing.T, group, headline, words string) Entry {
t.Helper()
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
key, err := delivery.MergeAskKey(group, heads)
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
key, err := meshdeliveryclient.MergeAskKey(group, heads)
if err != nil {
t.Fatal(err)
}
a := oc.Ask{ID: delivery.AskID(key), Headline: headline, Explanation: words, Who: occ.Operator, Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
a := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: headline, Explanation: words, Who: occ.Operator, Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
v, err := delivery.EncodeAsk(delivery.Ask{Group: group, Heads: heads, Ask: rawAsk(t, a)})
v, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: group, Heads: askHeads(heads), Ask: rawAsk(t, a)})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AsksBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: AsksBucket, Key: key, Value: v, At: t0}
}
// answerEntry is the teller's answer to the first asking of the key; answerToAsking to the asking with this router id. The
// answer's warrant is `<router id>@<time>` (teller.go warrantID).
func answerEntry(t *testing.T, key string) Entry { return answerToAsking(t, key, delivery.AskID(key)) }
func answerEntry(t *testing.T, key string) Entry {
return answerToAsking(t, key, meshdeliveryclient.AskID(key))
}
func answerToAsking(t *testing.T, key, id string) Entry {
t.Helper()
v, err := delivery.EncodeAnswer(delivery.Answer{Option: "approve", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
v, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "approve", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AnswersBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: AnswersBucket, Key: key, Value: v, At: t0}
}
func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1
@@ -65,11 +69,11 @@ func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/
t.Errorf("an answered ask is listed: %+v", got)
}
// An answer taken away leaves the ask open again; an ask taken away leaves the list.
fill(m, Entry{Bucket: delivery.AnswersBucket, Key: e.Key, Deleted: true})
fill(m, Entry{Bucket: AnswersBucket, Key: e.Key, Deleted: true})
if got := m.Snapshot().Asks; len(got) != 1 {
t.Errorf("asks after the answer was taken away = %+v", got)
}
fill(m, Entry{Bucket: delivery.AsksBucket, Key: e.Key, Deleted: true})
fill(m, Entry{Bucket: AsksBucket, Key: e.Key, Deleted: true})
if got := m.Snapshot().Asks; len(got) != 0 {
t.Errorf("a withdrawn ask is listed: %+v", got)
}
@@ -86,9 +90,9 @@ func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped silently
m := New()
fill(m, Entry{Bucket: delivery.AsksBucket, Key: "merge.g.0123456789ab", Value: []byte("{"), At: t0})
fill(m, Entry{Bucket: AsksBucket, Key: "merge.g.0123456789ab", Value: []byte("{"), At: t0})
s := m.Snapshot()
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != delivery.AsksBucket {
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != AsksBucket {
t.Errorf("snapshot = %+v", s)
}
}
@@ -138,8 +142,8 @@ func TestTheSnapshotsJSONCarriesTheAsks(t *testing.T) { // 1345/AC2
func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed
m := New()
fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", delivery.PhaseRemoved, delivery.HealthOK, ""), t0),
haveE(t, "novox", "stray", have(t, "b2", delivery.PhaseRunning, delivery.HealthOK, ""), t0))
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()
if r := find(t, s, "novox", "mesh-delivery"); r.Status != StatusRemoved {
t.Errorf("row = %+v, want status %q", r, StatusRemoved)
@@ -168,15 +172,15 @@ func TestAnAnswerToAnEarlierAskingDoesNotHideTheNextAsking(t *testing.T) { // R2
t.Fatalf("asking 1 is answered and listed: %+v", got)
}
// The gate rewrites the key at asking 2; only asking 1's answer exists.
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
a := oc.Ask{ID: delivery.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
a := oc.Ask{ID: meshdeliveryclient.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
v, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Asking: 2, Ask: rawAsk(t, a)})
v, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Asking: 2, Ask: rawAsk(t, a)})
if err != nil {
t.Fatal(err)
}
fill(m, Entry{Bucket: delivery.AsksBucket, Key: e.Key, Value: v, At: t0})
fill(m, Entry{Bucket: AsksBucket, Key: e.Key, Value: v, At: t0})
if got := m.Snapshot().Asks; len(got) != 1 {
t.Fatalf("asking 2 is not listed while only asking 1's answer exists: %+v", got)
}
@@ -195,3 +199,12 @@ func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
}
return b
}
// askHeads is the heads of a merge ask as the seat carries them.
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
for i, h := range heads {
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
}
return out
}
@@ -5,14 +5,13 @@ import (
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The backlog of the build seats (novox/hq ADR 0525 §4, issue 1887 A8a): for each language and kind of ask, how many asks wait
// on the seat's stream for a node to pull them, how many a node holds without having finished them, and how old the oldest
// waiting ask is. It is read from the stream's shared pull worker (delivery.AskWorker) when the view is asked: NumPending
// waiting ask is. It is read from the stream's shared pull worker (buildstep.AskWorker) when the view is asked: NumPending
// waits, NumAckPending is held by a node, and the oldest waiting ask is the stream's message at the worker's next sequence.
// BacklogRow is one language's kind of ask: Waiting asks not yet pulled, Held asks pulled by a node and not yet ended, and the
@@ -42,11 +41,11 @@ func (b *Backlog) Rows() []BacklogRow {
now = b.Now
}
out := []BacklogRow{}
for _, lang := range delivery.AskLanguages {
stream, _ := delivery.AskStream(lang)
for _, kind := range delivery.AskKinds {
worker, _ := delivery.AskWorker(lang, kind)
subject, _ := delivery.AskSubject(lang, kind)
for _, lang := range buildstep.AskLanguages {
stream, _ := buildstep.AskStream(lang)
for _, kind := range buildstep.AskKinds {
worker, _ := buildstep.AskWorker(lang, kind)
subject, _ := buildstep.AskSubject(lang, kind)
row := BacklogRow{Language: lang, Kind: kind}
waiting, held, next, err := b.Info(stream, worker)
if err != nil {
@@ -58,8 +57,8 @@ func (b *Backlog) Rows() []BacklogRow {
if waiting > 0 {
body, err := b.Message(stream, subject, next)
if err == nil {
var a delivery.StepAsk
a, err = delivery.DecodeStepAsk(body)
var a buildstep.StepAsk
a, err = buildstep.DecodeStepAsk(body)
if err == nil {
row.Oldest, row.OldestSecs = a.At, int64(now().Sub(a.At).Seconds())
}
@@ -8,13 +8,13 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
func stepAsk(t *testing.T, id, kind string, at time.Time) []byte {
t.Helper()
b, err := delivery.EncodeStepAsk(delivery.StepAsk{ID: id, Kind: kind, Language: delivery.AskGo, Asker: "mesh-delivery-checker", At: at,
Check: &delivery.CheckJob{Fetch: map[string]any{"repository": "novox/x", "commit": "0123456789abcdef0123456789abcdef01234567"}, Levels: []string{delivery.LevelUnit}}})
b, err := buildstep.EncodeStepAsk(buildstep.StepAsk{ID: id, Kind: kind, Language: buildstep.AskGo, Asker: "mesh-delivery-checker", At: at,
Check: &buildstep.CheckJob{Fetch: buildstep.Fetch{Repository: "novox/x", Commit: "0123456789abcdef0123456789abcdef01234567"}, Levels: []string{buildstep.LevelUnit}}})
if err != nil {
t.Fatal(err)
}
@@ -23,9 +23,9 @@ func stepAsk(t *testing.T, id, kind string, at time.Time) []byte {
// fakeSeats is the check worker of the go seat with three asks waiting and one held, the oldest at t0.
func fakeSeats(t *testing.T, now time.Time) *Backlog {
worker, _ := delivery.AskWorker(delivery.AskGo, delivery.AskCheck)
subject, _ := delivery.AskSubject(delivery.AskGo, delivery.AskCheck)
oldest := stepAsk(t, "m0000001aaaaaa", delivery.AskCheck, now.Add(-90*time.Second))
worker, _ := buildstep.AskWorker(buildstep.AskGo, buildstep.AskCheck)
subject, _ := buildstep.AskSubject(buildstep.AskGo, buildstep.AskCheck)
oldest := stepAsk(t, "m0000001aaaaaa", buildstep.AskCheck, now.Add(-90*time.Second))
return &Backlog{Now: func() time.Time { return now },
Info: func(stream, w string) (uint64, uint64, uint64, error) {
switch w {
@@ -4,7 +4,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// I5 (issue 1851): from R4e the indexer writes the new form alone, and a module's fingerprint is built by the builder's key
@@ -12,17 +13,17 @@ import (
const fp2A = "src2:3333333333333333333333333333333333333333333333333333333333333333"
func builderKey(t *testing.T, module, fp2 string, b delivery.BuilderBuild) Entry {
func builderKey(t *testing.T, module, fp2 string, b builds.BuilderBuild) Entry {
t.Helper()
key, err := delivery.BuilderBuildKey(module, fp2)
key, err := builds.BuilderBuildKey(module, fp2)
if err != nil {
t.Fatal(err)
}
data, err := delivery.EncodeBuilderBuild(b)
data, err := builds.EncodeBuilderBuild(b)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.BuilderBuildsBucket, Key: key, Value: data, At: t0}
return Entry{Bucket: builds.BuilderBuildsBucket, Key: key, Value: data, At: t0}
}
func TestAFingerprintOfTheNewFormIsShownAgainstItsBuilderKey(t *testing.T) { // I5
@@ -30,13 +31,13 @@ func TestAFingerprintOfTheNewFormIsShownAgainstItsBuilderKey(t *testing.T) { //
manifest := strings.Repeat("a", 64)
d := "sha256:" + strings.Repeat("2", 64)
fill(m,
modEntry(t, "a", delivery.Module{FP2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "a", fp2A, delivery.BuilderBuild{State: delivery.BuildDone, By: "shanks.mesh-delivery-builder", Manifest: manifest,
Artifacts: []delivery.BuiltArtifact{{Name: "program", Kind: "bundle", Digest: d, Reference: "artifact-store://a/program/blobs/" + d}}}),
modEntry(t, "f", delivery.Module{FP2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "f", fp2A, delivery.BuilderBuild{State: delivery.BuildFailed, By: "shanks.mesh-delivery-builder", Manifest: manifest, Error: "not stored: program: 507"}),
modEntry(t, "n", delivery.Module{FP2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "r", delivery.Module{None2: "toolchain not yet reported", Repository: "novox/mesh-delivery", Commit: hA}))
modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "a", fp2A, builds.BuilderBuild{State: builds.BuildDone, By: "shanks.mesh-delivery-builder", Manifest: manifest,
Artifacts: []builds.BuiltArtifact{{Name: "program", Kind: "bundle", Digest: d, Reference: "artifact-store://a/program/blobs/" + d}}}),
modEntry(t, "f", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "f", fp2A, builds.BuilderBuild{State: builds.BuildFailed, By: "shanks.mesh-delivery-builder", Manifest: manifest, Error: "not stored: program: 507"}),
modEntry(t, "n", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Manifest: manifest, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "r", meshcatalog.ModulesByModuleValue{None2: "toolchain not yet reported", Repository: "novox/mesh-delivery", Commit: hA}))
s := m.Snapshot()
if r := fpRow(t, s, "a"); r.State != FPBuilt || r.Indexer != fp2A {
t.Errorf("a = %+v, want built with the indexer's fp2", r)
@@ -2,12 +2,16 @@ package view
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"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"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
const (
@@ -17,37 +21,37 @@ const (
fG = "src1:2222222222222222222222222222222222222222222222222222222222222222"
)
func candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) Entry {
func candidate(t *testing.T, group string, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) Entry {
t.Helper()
heads := make([]delivery.Head, len(pulls))
heads := make([]meshdeliveryclient.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
hash, err := delivery.HeadsHash(heads)
hash, err := meshdeliveryclient.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
b, err := delivery.EncodeCandidate(delivery.Candidate{Group: group, HeadsHash: hash, Pulls: pulls})
b, err := json.Marshal(meshcatalog.CandidatesByGroupValue{Group: group, Heads: hash, Pulls: pulls})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.CandidatesBucket, Key: group, Value: b, At: t0}
return Entry{Bucket: CandidatesBucket, Key: group, Value: b, At: t0}
}
func pullOf(repo string, n int, head string, modules ...string) delivery.CandidatePull {
func pullOf(repo string, n int, head string, modules ...string) meshcatalog.CandidatesByGroupValuePullsItem {
if modules == nil {
modules = []string{}
}
return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, Modules: modules}
return meshcatalog.CandidatesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, Modules: modules}
}
func modEntry(t *testing.T, name string, m delivery.Module) Entry {
func modEntry(t *testing.T, name string, m meshcatalog.ModulesByModuleValue) Entry {
t.Helper()
b, err := delivery.EncodeModule(m)
b, err := meshcatalogclient.EncodeModule(m)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.ModulesBucket, Key: name, Value: b, At: t0}
return Entry{Bucket: ModulesBucket, Key: name, Value: b, At: t0}
}
func fpRow(t *testing.T, s Snapshot, module string) FingerprintRow {
@@ -81,7 +85,7 @@ func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1
if len(g[1].Pulls) != 1 || len(g[1].Pulls[0].Modules) != 0 {
t.Errorf("feat/y = %+v", g[1])
}
m.Apply(Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Deleted: true})
m.Apply(Entry{Bucket: CandidatesBucket, Key: "feat/x", Deleted: true})
if g := m.Snapshot().Groups; len(g) != 1 || g[0].Group != "feat/y" {
t.Errorf("after the delete: %+v", g)
}
@@ -92,8 +96,8 @@ func TestSnapshotListsTheCandidateGroups(t *testing.T) { // 1241/AC1
func TestAModuleOfTheOldFormIsComparedWithNoBuild(t *testing.T) {
m := New()
fill(m,
modEntry(t, "a", delivery.Module{FP: fF, Repository: "novox/mesh-delivery", Commit: hA}),
modEntry(t, "b", delivery.Module{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}))
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}))
s := m.Snapshot()
for _, name := range []string{"a", "b"} {
if r := fpRow(t, s, name); r.State != FPNone || !(strings.Contains(r.Reason, "new form") || strings.Contains(r.Reason, "release 4")) || r.Build != "" {
@@ -106,9 +110,9 @@ func shown(t *testing.T) *Model {
m := New()
fill(m,
candidate(t, "feat/x", pullOf("novox/mesh-delivery", 12, hA, "a"), pullOf("novox/mesh-catalog", 300, hB, "c")),
modEntry(t, "a", delivery.Module{FP2: fp2A, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "a", fp2A, delivery.BuilderBuild{State: delivery.BuildFailed, Error: "the compile failed"}),
modEntry(t, "b", delivery.Module{None: "no tree", Repository: "novox/mesh-delivery", Commit: hA}))
modEntry(t, "a", meshcatalog.ModulesByModuleValue{Fp2: fp2A, Repository: "novox/mesh-delivery", Commit: hA}),
builderKey(t, "a", fp2A, builds.BuilderBuild{State: builds.BuildFailed, Error: "the compile failed"}),
modEntry(t, "b", meshcatalog.ModulesByModuleValue{None: "no tree", Repository: "novox/mesh-delivery", Commit: hA}))
return m
}
@@ -132,8 +136,8 @@ func TestTableAndPagePrintTheGroupsAndTheComparison(t *testing.T) { // 1241/AC6
func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silently
m := New()
fill(m, Entry{Bucket: delivery.CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0},
Entry{Bucket: delivery.ModulesBucket, Key: "a", Value: []byte("{"), At: t0})
fill(m, Entry{Bucket: CandidatesBucket, Key: "feat/x", Value: []byte("{"), At: t0},
Entry{Bucket: ModulesBucket, Key: "a", Value: []byte("{"), At: t0})
s := m.Snapshot()
if len(s.Unreadable) != 2 || len(s.Groups) != 0 || len(s.Fingerprints) != 0 {
t.Errorf("snapshot = %+v", s)
@@ -11,7 +11,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
const (
@@ -59,7 +60,7 @@ type ModuleHistory struct {
func (r Reader) ModuleHistory(module string) (ModuleHistory, error) {
module = strings.TrimSpace(module)
out := ModuleHistory{Module: module, Note: "newest first; the modules state keeps 20 values a key (its history), so an older one is gone"}
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
return out, err
}
@@ -77,11 +78,11 @@ func (r Reader) ModuleHistory(module string) (ModuleHistory, error) {
row := ModuleRevision{Revision: rev.Revision, At: rev.At, Op: rev.Op}
if len(rev.Value) > 0 && string(rev.Value) != "null" {
// Read field by field, whatever the value's version, so an older or newer writer's value still shows.
var m delivery.Module
var m meshcatalog.ModulesByModuleValue
if err := json.Unmarshal(rev.Value, &m); err != nil {
row.Unreadable = err.Error()
} else {
row.Repository, row.Commit, row.Src, row.FP2, row.None2, row.Manifest, row.Path = m.Repository, m.Commit, m.Src, m.FP2, m.None2, m.Manifest, m.Path
row.Repository, row.Commit, row.Src, row.FP2, row.None2, row.Manifest, row.Path = m.Repository, m.Commit, uint64(m.Src), m.Fp2, m.None2, m.Manifest, m.Path
}
}
out.Values = append(out.Values, row)
@@ -11,7 +11,10 @@ import (
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/delivery"
"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-view/viewstate"
)
// The assigner's holds, listed and answered from the operator's command (issue 1906 note 23). A hold ask answered once (on the
@@ -29,14 +32,14 @@ const (
type heldEntry struct {
module, fp string
value delivery.Held
value meshwantsclient.Held
err string
at time.Time
}
// applyHeld takes a change of a held key of the assigner's wants, and says whether the key was one.
func (m *Model) applyHeld(e Entry) bool {
module, fp, err := delivery.ParseHeldKey(e.Key)
module, fp, err := meshwantsclient.ParseHeldKey(e.Key)
if err != nil {
return false
}
@@ -44,7 +47,7 @@ func (m *Model) applyHeld(e Entry) bool {
delete(m.holds, e.Key)
return true
}
h, err := delivery.DecodeHeld(e.Value)
h, err := meshwantsclient.DecodeHeld(e.Value)
m.holds[e.Key] = heldEntry{module: module, fp: fp, value: h, err: errText(err), at: e.At}
return true
}
@@ -74,7 +77,7 @@ func (m *Model) Holds() []HoldRow {
out := []HoldRow{}
for _, h := range m.holds {
row := HoldRow{Module: h.module, FP: h.fp, Node: h.value.Node, Words: h.value.Words, Since: h.at, Error: h.err}
if key, err := delivery.HoldAskKey(h.module, h.fp); err == nil {
if key, err := asks.HoldAskKey(h.module, h.fp); err == nil {
if a, ok := m.aasks[key]; ok && a.err == "" {
row.AskKey, row.Asking = key, a.ask.ID
if ans, ok := m.answerOf[key]; ok && m.answers[key] == a.ask.ID {
@@ -106,9 +109,9 @@ func (m *Model) AnswerHold(module, fp, option, words string) (HoldRow, error) {
return HoldRow{}, fmt.Errorf("a hold is answered %s or %s, not %q", HoldRelease, HoldKeep, option)
}
var match []HoldRow
want := strings.TrimPrefix(strings.TrimSpace(fp), delivery.SourcePrefix2)
want := strings.TrimPrefix(strings.TrimSpace(fp), meshcatalogclient.SourcePrefix2)
for _, r := range m.Holds() {
if r.Module == module && want != "" && strings.HasPrefix(strings.TrimPrefix(r.FP, delivery.SourcePrefix2), want) {
if r.Module == module && want != "" && strings.HasPrefix(strings.TrimPrefix(r.FP, meshcatalogclient.SourcePrefix2), want) {
match = append(match, r)
}
}
@@ -134,11 +137,11 @@ func (m *Model) AnswerHold(module, fp, option, words string) (HoldRow, error) {
if w == nil {
return r, fmt.Errorf("the view cannot write its answers here")
}
key, err := delivery.ViewAnswerKey(r.AskKey)
key, err := viewstate.ViewAnswerKey(r.AskKey)
if err != nil {
return r, err
}
body, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: a.ask.ID, Option: option, Words: strings.TrimSpace(words)})
body, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: a.ask.ID, Option: option, Words: strings.TrimSpace(words)})
if err != nil {
return r, err
}
@@ -4,30 +4,33 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"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-view/viewstate"
)
// heldE is the assigner's hold of module at fp, as it writes it in its wants.
func heldE(t *testing.T, module, fp, node, words string) Entry {
t.Helper()
key, err := delivery.HeldKey(module, fp)
key, err := meshwantsclient.HeldKey(module, fp)
if err != nil {
t.Fatal(err)
}
v, err := delivery.EncodeHeld(delivery.Held{Node: node, Words: words})
v, err := meshwantsclient.EncodeHeld(meshwantsclient.Held{Node: node, Words: words})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AssignerWantsBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: AssignerWantsBucket, Key: key, Value: v, At: t0}
}
func answered(t *testing.T, key, id, option string) Entry {
t.Helper()
v, err := delivery.EncodeAnswer(delivery.Answer{Option: option, Warrant: id + "@2026-10-25T04:00:00Z", Words: "the operator, via telegram, chose " + option})
v, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: option, Warrant: id + "@2026-10-25T04:00:00Z", Words: "the operator, via telegram, chose " + option})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AnswersBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: AnswersBucket, Key: key, Value: v, At: t0}
}
// 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
@@ -61,11 +64,11 @@ func TestAKeptHoldCanBeReleased(t *testing.T) {
m.SetWriter(w)
e, a := holdAsk(t)
fill(m, e, heldE(t, "plex", fpNew, "ace", "health failed"), answered(t, e.Key, a.ID, "keep"))
short := strings.TrimPrefix(fpNew, delivery.SourcePrefix2)[:8]
short := strings.TrimPrefix(fpNew, meshcatalogclient.SourcePrefix2)[:8]
if _, err := m.AnswerHold("plex", short, HoldRelease, "it was a flake"); err != nil {
t.Fatal(err)
}
va, err := delivery.DecodeViewAnswer(w.puts[e.Key+".view"])
va, err := viewstate.DecodeViewAnswer(w.puts[e.Key+".view"])
if err != nil || va.Asking != a.ID || va.Option != HoldRelease || va.Words != "it was a flake" {
t.Fatalf("the view's answer = %+v, %v", va, err)
}
@@ -0,0 +1,14 @@
package view
import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-sdk/go/motion"
)
// motionOf derives where one module's want stands on one node from the want and the have as their seats carry them, through
// mesh-sdk's seat-neutral derivation (issue 1952 S7), which the condition keeper's hold uses too.
func motionOf(w meshwantsclient.Want, wanted bool, h nodeengine.HaveByNodeModuleValue, has bool, words string, stuck bool) motion.Motion {
return motion.MotionOf(motion.Want{FP: w.Fp, Composed: w.Composed, Removed: w.Removed}, wanted,
motion.Have{FP: h.Fp, Composed: h.Composed, Phase: h.Phase, Health: h.Health, Detail: h.Detail}, has, words, stuck)
}
@@ -0,0 +1,88 @@
package view
import (
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The holders, states and buckets the view names, taken from its own manifest (module.json: its `reads`, `invokes` and
// `consumes`); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder (novox/hq
// ADR 0625 §4, ADR 0634 §1). The indexer's module and its reads are the catalog client's.
const (
IndexerModule = meshcatalogclient.Module
AssignerModule = "mesh-delivery-assigner"
WatchmanModule = "mesh-delivery-watchman"
GateModule = "mesh-delivery-gate"
TellerModule = "mesh-delivery-teller"
VaultModule = "mesh-vault"
EngineModule = "node-engine"
// The indexer's states the view reads.
ModulesBucket = IndexerModule + "_modules"
CandidatesBucket = IndexerModule + "_candidates"
TakenBucket = IndexerModule + "_taken"
PackagesState = "packages"
PackagesBucket = IndexerModule + "_" + PackagesState
// IndexerPredefinedState and IndexerConfigCandidatesState are the indexer's states of the foundation's predefined list
// and of the config candidates; PredefinedListKey is the one key of the first.
IndexerPredefinedState = "predefined"
PredefinedListKey = "list"
IndexerConfigCandidatesState = "config-candidates"
// The assigner's states.
PlacementState = "placement"
PlacementBucket = AssignerModule + "_" + PlacementState
AssignerWantsBucket = AssignerModule + "_wants"
AssignerAsksBucket = AssignerModule + "_asks"
// The gate's and the teller's states.
SourceBucket = GateModule + "_source"
AsksBucket = GateModule + "_asks"
AnswersBucket = TellerModule + "_answers"
// The watchman's and the node-engine's states.
PresenceBucket = WatchmanModule + "_presence"
StuckBucket = WatchmanModule + "_stuck"
HaveBucket = EngineModule + "_have"
// StepsState is the state of steps each build module keeps (reads: mesh-build-<language>.steps), whose pauses are buildstep's.
StepsState = "steps"
// The revisions each of those states keeps per key.
AsksHistory = 5
AnswersHistory = 5
AssignerAsksHistory = 5
// SignersRead is the vault's state of signing keys as a manifest's `reads` names it, SignersKeyKey its one key.
SignersRead = VaultModule + ".signers"
SignersKeyKey = "key"
)
// CandidatesKey is the key, in the indexer's candidates, of a group: the group name as a key part. ParseCandidatesKey is its inverse.
func CandidatesKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("view: the group name is empty")
}
return keys.EscapeName(group), nil
}
// ParseCandidatesKey is the inverse of CandidatesKey.
func ParseCandidatesKey(key string) (string, error) {
group, err := keys.UnescapeName(key)
if err != nil || group == "" {
return "", fmt.Errorf("view: %q is not a candidates key: %v", key, err)
}
return group, nil
}
// PackagesKey is the key, in the indexer's packages, of a mesh Go module (its module path): the path as a key part. It refuses
// a path that is no mesh Go module's.
func PackagesKey(module string) (string, error) {
if _, _, ok := trunk.PackageRepository(module); !ok {
return "", fmt.Errorf("view: %q is no mesh Go module's path", module)
}
return keys.EscapeName(module), nil
}
@@ -6,7 +6,12 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"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"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// How far each repository reached (novox/hq ADR 0556 clauses 2 and 3, issue 1280). The view keeps, per repository the gate's
@@ -22,8 +27,8 @@ 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.
var reachBuckets = []string{delivery.ModulesBucket, delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.SourceBucket,
delivery.TakenBucket, ReachHistory(delivery.ModulesBucket), ReachHistory(delivery.SourceBucket)}
var reachBuckets = []string{ModulesBucket, AssignerWantsBucket, HaveBucket, SourceBucket,
TakenBucket, ReachHistory(ModulesBucket), ReachHistory(SourceBucket)}
// ReachWrite is a reach value to write under its key in the view's state reach.
type ReachWrite struct {
@@ -43,32 +48,32 @@ func (m *Model) SetReachWriter(put func(ReachWrite) error) {
// the gate's source (each revision's heads).
func (m *Model) applyHistory(e Entry) {
switch e.Bucket {
case delivery.ModulesBucket:
case ModulesBucket:
if e.Deleted {
delete(m.modHist, e.Key)
return
}
mod, err := delivery.DecodeModule(e.Value)
if err != nil || mod.FP2 == "" || mod.Commit == "" {
mod, err := meshcatalogclient.DecodeModule(e.Value)
if err != nil || mod.Fp2 == "" || mod.Commit == "" {
return
}
h := m.modHist[e.Key]
if n := len(h); n > 0 && h[n-1].FP2 == mod.FP2 && h[n-1].Commit == mod.Commit {
if n := len(h); n > 0 && h[n-1].Fp2 == mod.Fp2 && h[n-1].Commit == mod.Commit {
if mod.Src != 0 {
h[n-1].Src = mod.Src
}
return
}
h = append(h, mod)
if len(h) > 2*delivery.MostChanges {
h = h[len(h)-delivery.MostChanges:]
if len(h) > 2*viewstate.MostChanges {
h = h[len(h)-viewstate.MostChanges:]
}
m.modHist[e.Key] = h
case delivery.SourceBucket:
if e.Key != delivery.SourceMainKey || e.Deleted || e.Revision == 0 {
case SourceBucket:
if e.Key != meshdeliveryclient.SourceMainKey || e.Deleted || e.Revision == 0 {
return
}
src, err := delivery.DecodeSource(e.Value)
src, err := meshdeliveryclient.DecodeSource(e.Value)
if err != nil {
return
}
@@ -87,7 +92,7 @@ func (m *Model) headsOf(repository string) []headAt {
var out []headAt
for _, rev := range slices.Sorted(maps.Keys(m.srcHist)) {
for r, head := range m.srcHist[rev] {
if !delivery.SameRepository(r, repository) || head == "" {
if !trunk.SameRepository(r, repository) || head == "" {
continue
}
if n := len(out); n == 0 || !sameCommit(out[n-1].commit, head) {
@@ -131,10 +136,10 @@ func (m *Model) wantKeys() map[string][]string {
}
// deriveReach is the reach of one repository from the model as it stands; wants are the wants' keys by module (wantKeys).
func (m *Model) deriveReach(repository string, wants map[string][]string) delivery.Reach {
r := delivery.Reach{Repository: delivery.RepositoryName(repository), Modules: map[string]delivery.ModuleReach{}}
func (m *Model) deriveReach(repository string, wants map[string][]string) viewstate.Reach {
r := viewstate.Reach{Repository: trunk.RepositoryName(repository), Modules: map[string]viewstate.ModuleReach{}}
heads := m.headsOf(repository)
key, _ := delivery.RepositoryKey(repository)
key, _ := trunk.RepositoryKey(repository)
if t, ok := m.taken[key]; ok {
r.Indexed = t.Merge
}
@@ -142,7 +147,7 @@ func (m *Model) deriveReach(repository string, wants map[string][]string) delive
var holds []string
for _, name := range slices.Sorted(maps.Keys(m.modules)) {
me := m.modules[name]
if me.err != "" || me.value.FP2 == "" || !delivery.SameRepository(me.value.Repository, repository) {
if me.err != "" || me.value.Fp2 == "" || !trunk.SameRepository(me.value.Repository, repository) {
continue
}
mr, b, words := m.moduleReach(name, me.value, heads, wants[name])
@@ -184,14 +189,14 @@ func (m *Model) deriveReach(repository string, wants map[string][]string) delive
// moduleReach is how far one module reached, the source revision it holds the reached head before (noBound when none), and
// what holds it, in words. A node counts for the fingerprint its running have runs, healthy or not (ADR 0556 clause 2).
func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wantKeys []string) (delivery.ModuleReach, uint64, string) {
mr := delivery.ModuleReach{FP: mod.FP2}
func (m *Model) moduleReach(name string, mod meshcatalog.ModulesByModuleValue, heads []headAt, wantKeys []string) (viewstate.ModuleReach, uint64, string) {
mr := viewstate.ModuleReach{FP: mod.Fp2}
hist := m.modHist[name]
if n := len(hist); n == 0 || hist[n-1].FP2 != mod.FP2 || hist[n-1].Commit != mod.Commit {
if n := len(hist); n == 0 || hist[n-1].Fp2 != mod.Fp2 || hist[n-1].Commit != mod.Commit {
hist = append(slices.Clone(hist), mod) // the value watched now is the newest, whatever the history watch has given
}
for i := len(hist) - 1; i >= 0 && len(mr.Changes) < delivery.MostChanges; i-- {
c := delivery.Change{Commit: hist[i].Commit, FP: hist[i].FP2, Src: hist[i].Src}
for i := len(hist) - 1; i >= 0 && len(mr.Changes) < viewstate.MostChanges; i-- {
c := viewstate.Change{Commit: hist[i].Commit, FP: hist[i].Fp2, Src: uint64(hist[i].Src)}
if c.Src == 0 {
c.Src = revOf(heads, c.Commit)
}
@@ -207,7 +212,7 @@ func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wa
for _, key := range wantKeys {
node, _ := splitKey(key)
w := m.wants[key]
if w.err != "" || w.value.Removed || w.value.FP == "" {
if w.err != "" || w.value.Removed || w.value.Fp == "" {
continue
}
if away[node] {
@@ -218,24 +223,24 @@ func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wa
mr.Nodes++
h, ok := m.haves[key]
switch {
case !ok || h.err != "" || h.have.FP == "":
case !ok || h.err != "" || h.have.Fp == "":
waiting = append(waiting, fmt.Sprintf("%s on %s runs nothing yet", name, node))
unset = waiting[len(waiting)-1]
continue
case !runs(h.have.Phase):
waiting = append(waiting, fmt.Sprintf("%s on %s is %s at %s, not running", name, node, h.have.Phase, short(h.have.FP)))
waiting = append(waiting, fmt.Sprintf("%s on %s is %s at %s, not running", name, node, h.have.Phase, short(h.have.Fp)))
unset = waiting[len(waiting)-1]
continue
case h.have.FP != w.value.FP:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet the %s it wants", name, node, short(h.have.FP), short(w.value.FP)))
case h.have.Health == delivery.HealthFailed:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s and its health check failed", name, node, short(h.have.FP)))
case h.have.Health != delivery.HealthOK:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet healthy", name, node, short(h.have.FP)))
case h.have.Fp != w.value.Fp:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet the %s it wants", name, node, short(h.have.Fp), short(w.value.Fp)))
case h.have.Health == nodeengineclient.HealthFailed:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s and its health check failed", name, node, short(h.have.Fp)))
case h.have.Health != nodeengineclient.HealthOK:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet healthy", name, node, short(h.have.Fp)))
}
i := slices.IndexFunc(mr.Changes, func(c delivery.Change) bool { return c.FP == h.have.FP })
i := slices.IndexFunc(mr.Changes, func(c viewstate.Change) bool { return c.FP == h.have.Fp })
if i < 0 {
unset = fmt.Sprintf("%s on %s runs %s, which is not among its %d known changes", name, node, short(h.have.FP), len(mr.Changes))
unset = fmt.Sprintf("%s on %s runs %s, which is not among its %d known changes", name, node, short(h.have.Fp), len(mr.Changes))
waiting = append(waiting, unset)
continue
}
@@ -274,13 +279,13 @@ func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wa
// runs is whether a have in this phase runs the fingerprint it names: running, being checked after its apply, or held after a
// failed backup, which keeps running what it has (ADR 0539 clause 16). A have backing up or applying, failed or removed runs
// nothing the derivation can count.
func runs(p delivery.Phase) bool {
return p == delivery.PhaseRunning || p == delivery.PhaseChecking || p == delivery.PhaseHeld
func runs(p string) bool {
return p == nodeengineclient.PhaseRunning || p == nodeengineclient.PhaseChecking || p == nodeengineclient.PhaseHeld
}
// short is a fingerprint's first twelve characters, after its form.
func short(fp string) string {
fp = strings.TrimPrefix(fp, delivery.SourcePrefix2)
fp = strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
if len(fp) > 12 {
fp = fp[:12]
}
@@ -307,13 +312,13 @@ func (m *Model) reachNow() ([]ReachWrite, []DeliveredEvent) {
var events []DeliveredEvent
wants := m.wantKeys()
for _, repository := range slices.Sorted(maps.Keys(m.source.heads)) {
key, err := delivery.RepositoryKey(repository)
key, err := trunk.RepositoryKey(repository)
if err != nil {
continue
}
r := m.deriveReach(repository, wants)
if prev, ok := m.reach[key]; !ok || !delivery.SameReach(prev, r) {
if data, err := delivery.EncodeReach(r); err == nil {
if prev, ok := m.reach[key]; !ok || !viewstate.SameReach(prev, r) {
if data, err := viewstate.EncodeReach(r); err == nil {
m.reach[key] = r
writes = append(writes, ReachWrite{Key: key, Value: data})
}
@@ -332,7 +337,7 @@ func (m *Model) reachNow() ([]ReachWrite, []DeliveredEvent) {
// notYetOf are the merges at or before a repository's reached head that are not yet on the machines: the changes of each module
// a node wants with no floor (a node runs nothing countable of it), sorted. A consumer takes every other merge at or before
// the reached head as on the machines.
func notYetOf(r delivery.Reach) []string {
func notYetOf(r viewstate.Reach) []string {
var out []string
for _, mr := range r.Modules {
if mr.Nodes == 0 || mr.Floor != "" {
@@ -347,7 +352,7 @@ func notYetOf(r delivery.Reach) []string {
}
// Reach is the reach of every repository as the view derived it last, by key.
func (m *Model) Reach() map[string]delivery.Reach {
func (m *Model) Reach() map[string]viewstate.Reach {
m.mu.Lock()
defer m.mu.Unlock()
return maps.Clone(m.reach)
@@ -5,7 +5,16 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// 1280 (ADR 0556 clauses 2 and 3): the view derives, per repository, how far each module reached and the reached head, writes
@@ -16,45 +25,45 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
commitS, commitX, commitD := strings.Repeat("5", 40), strings.Repeat("e", 40), strings.Repeat("d", 40)
const fpMail = "src2:3333333333333333333333333333333333333333333333333333333333333333"
mod := func(module, fp, commit string, src uint64, history bool) Entry {
v, err := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: strings.Repeat("a", 64), Repository: web, Commit: commit, Src: src, Path: module})
v, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: strings.Repeat("a", 64), Repository: web, Commit: commit, Src: int64(src), Path: module})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.ModulesBucket, Key: module, Value: v, At: t0, History: history}
return Entry{Bucket: ModulesBucket, Key: module, Value: v, At: t0, History: history}
}
src := func(rev uint64, webHead string) Entry {
v, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: map[string]string{web: webHead, sdk: commitS}})
v, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{web: webHead, sdk: commitS}})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.SourceBucket, Key: delivery.SourceMainKey, Value: v, At: t0, Revision: rev, History: true}
return Entry{Bucket: SourceBucket, Key: meshdeliveryclient.SourceMainKey, Value: v, At: t0, Revision: rev, History: true}
}
taken := func(repository, merge string) Entry {
key, _ := delivery.RepositoryKey(repository)
v, err := delivery.EncodeTaken(delivery.Taken{Repository: repository, Merge: merge, At: t0})
key, _ := trunk.RepositoryKey(repository)
v, err := takein.EncodeTaken(takein.Taken{Repository: repository, Merge: merge, At: t0})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.TakenBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: TakenBucket, Key: key, Value: v, At: t0}
}
haveIn := func(node, module, fp string, phase delivery.Phase, health delivery.Health) Entry {
haveIn := func(node, module, fp string, phase string, health string) Entry {
e := haveFP(t, node, module, fp, health)
v, _ := delivery.EncodeHave(delivery.Have{FP: fp, Phase: phase, Health: health})
v, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: phase, Health: health})
e.Value = v
return e
}
// web changed at A (fpOld) and at C (fpNew); mail first appeared at A. The source named A, B, then C as web's head.
world := func(t *testing.T) []Entry {
cur, _ := delivery.DecodeSource(src(12, commitC).Value)
current := Entry{Bucket: delivery.SourceBucket, Key: delivery.SourceMainKey, Revision: 12, At: t0}
current.Value, _ = delivery.EncodeSource(cur)
cur, _ := meshdeliveryclient.DecodeSource(src(12, commitC).Value)
current := Entry{Bucket: SourceBucket, Key: meshdeliveryclient.SourceMainKey, Revision: 12, At: t0}
current.Value, _ = meshdeliveryclient.EncodeSource(cur)
return []Entry{src(10, commitA), src(11, commitB), src(12, commitC), current,
mod("web", fpOld, commitA, 10, true), mod("web", fpNew, commitC, 12, true), mod("mail", fpMail, commitA, 0, true),
mod("web", fpNew, commitC, 12, false), mod("mail", fpMail, commitA, 0, false),
taken(web, commitC), taken(sdk, commitS),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpNew, delivery.HealthOK),
wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthOK),
wantE(t, "ace", "mail", delivery.Want{FP: fpMail}), haveFP(t, "ace", "mail", fpMail, delivery.HealthOK)}
wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK),
wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthOK),
wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpMail}), haveFP(t, "ace", "mail", fpMail, nodeengineclient.HealthOK)}
}
for _, tc := range []struct {
name string
@@ -67,24 +76,24 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
}{
{name: "every node runs the newest build", reached: commitC},
{name: "a node lagging stops the head before the change it lacks", reached: commitB, holds: "web on g14",
then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}, waiting: "not yet the"},
then: []Entry{haveFP(t, "g14", "web", fpOld, nodeengineclient.HealthOK)}, waiting: "not yet the"},
// ADR 0603 §7: a node away holds no head back, is named, and holds it again once it returns still behind.
{name: "a node away lagging holds no head back and is named", reached: commitC, waiting: "g14 is away",
then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK), presenceE(t, "g14", false, t0)}},
then: []Entry{haveFP(t, "g14", "web", fpOld, nodeengineclient.HealthOK), presenceE(t, "g14", false, t0)}},
{name: "a node returned lagging holds the head again", reached: commitB, holds: "web on g14", waiting: "not yet the",
then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK), presenceE(t, "g14", false, t0), presenceE(t, "g14", true, t0)}},
then: []Entry{haveFP(t, "g14", "web", fpOld, nodeengineclient.HealthOK), presenceE(t, "g14", false, t0), presenceE(t, "g14", true, t0)}},
{name: "a want held back at an older build holds the head as a lag does", reached: commitB, holds: "web on g14 runs the build of",
then: []Entry{wantE(t, "g14", "web", delivery.Want{FP: fpOld, Newest: fpNew, Rule: 2}), haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}},
then: []Entry{wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpOld, Newest: fpNew, Rule: 2}), haveFP(t, "g14", "web", fpOld, nodeengineclient.HealthOK)}},
{name: "a failing health check is said and holds nothing", reached: commitC, waiting: "health check failed",
then: []Entry{haveFP(t, "g14", "web", fpNew, delivery.HealthFailed)}},
then: []Entry{haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthFailed)}},
{name: "a node with no running have holds no head back and its module's merges are not yet", reached: commitC, waiting: "not running",
notYet: []string{commitA, commitC},
then: []Entry{haveIn("g14", "web", fpNew, delivery.PhaseApplying, delivery.HealthUnknown)}},
then: []Entry{haveIn("g14", "web", fpNew, nodeengineclient.PhaseApplying, nodeengineclient.HealthUnknown)}},
{name: "an unrelated module not running leaves the head reached (the reviewer's probe)", reached: commitC,
notYet: []string{commitA},
then: []Entry{haveIn("ace", "mail", fpMail, delivery.PhaseFailed, delivery.HealthFailed)}},
then: []Entry{haveIn("ace", "mail", fpMail, nodeengineclient.PhaseFailed, nodeengineclient.HealthFailed)}},
{name: "a module no node wants holds nothing", reached: commitC,
then: []Entry{wantE(t, "ace", "mail", delivery.Want{Removed: true})}},
then: []Entry{wantE(t, "ace", "mail", meshwantsclient.Want{Removed: true})}},
{name: "the indexer not yet having taken the head in", reached: commitB, holds: "has not yet taken in",
then: []Entry{taken(web, commitB)}},
{name: "a change whose source revision is not known yet holds no head the source named", reached: commitC,
@@ -97,14 +106,14 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
current := head
current.History = false
gone := func(module string, history bool) Entry {
return Entry{Bucket: delivery.ModulesBucket, Key: module, Deleted: true, At: t0, History: history}
return Entry{Bucket: ModulesBucket, Key: module, Deleted: true, At: t0, History: history}
}
return []Entry{head, current, gone("web", true), gone("mail", true), gone("web", false), gone("mail", false), taken(web, commitD)}
}()},
// 1126: the view started while the bus did not answer its writes; its reach and its event are made at the next change it
// takes in, though the derivation is the same, so the state reach never keeps an older have than node_haves says.
{name: "a write and an event refused at the start are made at the next change", reached: commitC, refused: true,
then: []Entry{haveFP(t, "ace", "mail", fpMail, delivery.HealthOK)}},
then: []Entry{haveFP(t, "ace", "mail", fpMail, nodeengineclient.HealthOK)}},
} {
t.Run(tc.name, func(t *testing.T) {
m := New()
@@ -137,7 +146,7 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
}
refuse = false
fill(m, tc.then...)
key, _ := delivery.RepositoryKey(web)
key, _ := trunk.RepositoryKey(web)
r := m.Reach()[key]
if r.Reached != tc.reached {
t.Errorf("reached = %.8s, want %.8s (holds %v)", r.Reached, tc.reached, r.Holds)
@@ -149,20 +158,20 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
t.Errorf("web waits with %q, want words with %q", got, tc.waiting)
}
// A library reaches the head the indexer took in, with no module.
sk, _ := delivery.RepositoryKey(sdk)
sk, _ := trunk.RepositoryKey(sdk)
if lib := m.Reach()[sk]; lib.Reached != commitS || len(lib.Modules) != 0 {
t.Errorf("the library's reach = %+v", lib)
}
// Each key is written once with its last value; a reached head is said once, for its repository alone.
last := map[string]delivery.Reach{}
last := map[string]viewstate.Reach{}
for _, w := range written {
v, err := delivery.DecodeReach(w.Value)
v, err := viewstate.DecodeReach(w.Value)
if err != nil {
t.Fatalf("written %s: %v", w.Key, err)
}
last[w.Key] = v
}
if !delivery.SameReach(last[key], r) {
if !viewstate.SameReach(last[key], r) {
t.Errorf("the last value written is %+v, not the reach derived %+v", last[key], r)
}
if tc.reached != "" {
@@ -11,14 +11,14 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
)
func served(t *testing.T) (string, *Model) {
t.Helper()
m := New()
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", delivery.PhaseRunning, delivery.HealthOK, ""), t0))
fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0))
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "s")
if err != nil {
t.Fatal(err)
@@ -115,7 +115,7 @@ func watchmanSnapshot(t *testing.T) *Model {
t.Helper()
m := New()
fill(m, presenceE(t, "a", false, t0), stuckE(t, "node-engine_have", "a.x", "a report from a", t0, false),
Entry{Bucket: delivery.PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0})
Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0})
return m
}
@@ -6,8 +6,12 @@ import (
"sort"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/motion"
)
// `status` is the view's derivation (novox/hq ADR 0564 §10, Limits 7, issue 1952 V1): what is wrong, quiet and on its way,
@@ -15,7 +19,7 @@ import (
// summary and the probe D9 go with its lease. Each part comes from one bucket:
// - Conditions: the condition keeper's open conditions (mesh-conditions.conditions), urgent first, then oldest;
// - Quiet: the watchman's presence of each node that is offline, and its stuck keys not moved;
// - Moving: each module whose want and have differ, as mesh-sdk's delivery.MotionOf derives it (moving or stuck);
// - Moving: each module whose want and have differ, as mesh-sdk's motion.MotionOf derives it (moving or stuck);
// - Heals: what mesh-healers did in the last seven days (mesh-healers.attempts), by outcome.
// A bucket that cannot be read says so in its part's Unread, never as none.
@@ -26,8 +30,8 @@ const StatusTool = "view_status"
const (
StatusConditionsState = health.KeeperModule + "." + health.ConditionsState
StatusAttemptsState = health.HealersModule + "." + health.AttemptsState
StatusPresenceState = delivery.WatchmanModule + ".presence"
StatusStuckState = delivery.WatchmanModule + ".stuck"
StatusPresenceState = WatchmanModule + ".presence"
StatusStuckState = WatchmanModule + ".stuck"
)
// HealsWithin is how far back status counts the healers' acts.
@@ -134,12 +138,12 @@ func DeriveStatus(r StatusRead, now time.Time) Status {
st.QuietUnread = "the watchman's stuck keys cannot be read: " + serr.Error()
default:
for k, v := range presence {
if p, err := delivery.DecodePresence(v); err == nil && !p.Online {
if p, err := meshwatchdogsclient.DecodeNodePresence(v); err == nil && !p.Online {
st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "offline"})
}
}
for k, v := range stuck {
if s, err := delivery.DecodeStuck(v); err == nil && !s.Moved {
if s, err := meshwatchdogsclient.DecodeStuck(v); err == nil && !s.Moved {
st.Quiet = append(st.Quiet, StatusQuiet{Key: k, What: "stopped moving: it waited for " + s.Waited, Since: s.Since})
}
}
@@ -155,35 +159,35 @@ func DeriveStatus(r StatusRead, now time.Time) Status {
st.MovingUnread = "the nodes' haves cannot be read: " + herr.Error()
default:
for wk, v := range wants {
node, module, err := delivery.ParseWantKey(wk)
node, module, err := meshwantsclient.ParseWantKey(wk)
if err != nil {
continue
}
w, err := delivery.DecodeWant(v)
w, err := meshwantsclient.DecodeWant(v)
if err != nil {
continue
}
hk, _ := delivery.HaveKey(node, module)
var h delivery.Have
hk, _ := nodeengineclient.HaveKey(node, module)
var h nodeengine.HaveByNodeModuleValue
hv, has := haves[hk]
if has {
if h, err = delivery.DecodeHave(hv); err != nil {
if h, err = nodeengineclient.DecodeHave(hv); err != nil {
has = false
}
}
words, stopped := "", false
if sk, err := delivery.StuckKey(delivery.AssignerWantsBucket, wk); err == nil && serr == nil {
if s, err := delivery.DecodeStuck(stuck[sk]); err == nil && !s.Moved {
if sk, err := meshwatchdogsclient.StuckKey(AssignerWantsBucket, wk); err == nil && serr == nil {
if s, err := meshwatchdogsclient.DecodeStuck(stuck[sk]); err == nil && !s.Moved {
words, stopped = s.Waited, true
}
}
m := delivery.MotionOf(w, true, h, has, words, stopped)
if m.State == "" || m.State == delivery.MotionSettled {
m := motionOf(w, true, h, has, words, stopped)
if m.State == "" || m.State == motion.MotionSettled {
continue
}
row := StatusMoving{Node: node, Module: module, State: m.State, Outcome: m.Outcome, Words: m.Words, ToFP: w.FP}
row := StatusMoving{Node: node, Module: module, State: m.State, Outcome: m.Outcome, Words: m.Words, ToFP: w.Fp}
if has {
row.FromFP = h.FP
row.FromFP = h.Fp
}
st.Moving = append(st.Moving, row)
}
@@ -213,7 +217,7 @@ func DeriveStatus(r StatusRead, now time.Time) Status {
stuckMoving := false
for _, m := range st.Moving {
stuckMoving = stuckMoving || m.State == delivery.MotionStuck
stuckMoving = stuckMoving || m.State == motion.MotionStuck
}
st.Well = len(st.Conditions) == 0 && len(st.Quiet) == 0 && !stuckMoving &&
st.ConditionsUnread == "" && st.QuietUnread == "" && st.MovingUnread == ""
@@ -9,9 +9,14 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-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"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
@@ -32,7 +37,7 @@ const TiersTool = "view_tiers"
const TiersMost = 500
// Tiers is a source revision's tiers: Revision the source revision (the gate's source key's revision), Modules how many
// modules it moves (each whose fingerprint last changed at it, delivery.Module.FPSrc), Tiers those modules in order, the
// modules it moves (each whose fingerprint last changed at it, meshcatalog.ModulesByModuleValue.FPSrc), Tiers those modules in order, the
// first tier waiting on none of the others. Cut says when the answer holds fewer than Modules, Graph why the module graph
// could not be read (then every module is in the first tier), Cycle a cycle among them (its modules form the last
// tier), and Away the nodes left out.
@@ -78,7 +83,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
if r.SourceState.GetEntry == nil {
return Tiers{}, fmt.Errorf("the gate's source cannot be read here: name the source revision")
}
_, rev, err := r.SourceState.GetEntry(delivery.SourceMainKey)
_, rev, err := r.SourceState.GetEntry(meshdeliveryclient.SourceMainKey)
if err != nil {
return Tiers{}, fmt.Errorf("the gate's source: %w", err)
}
@@ -91,9 +96,9 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
if err != nil {
return Tiers{}, fmt.Errorf("the indexer's modules: %w", err)
}
moved := map[string]delivery.Module{}
moved := map[string]meshcatalog.ModulesByModuleValue{}
for key, raw := range modules {
if m, err := delivery.DecodeModule(raw); err == nil && m.FPSrc == revision {
if m, err := meshcatalogclient.DecodeModule(raw); err == nil && uint64(m.FpSrc) == revision {
moved[key] = m
}
}
@@ -179,7 +184,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
out.Tiers = append(out.Tiers, nil)
}
m := moved[n]
tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n], LeftOut: gr[n].LeftOut}
tm := TierModule{Module: n, FP: m.Fp2, None: m.None2, After: after[n], LeftOut: gr[n].LeftOut}
tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away)
if w, ok := held[n]; ok {
tm.Waits = append(tm.Waits, w)
@@ -193,8 +198,8 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) {
continue
}
tm := TierModule{Module: n, Waits: []TierWait{held[n]}}
if m, err := delivery.DecodeModule(modules[n]); err == nil {
tm.FP, tm.None = m.FP2, m.None2
if m, err := meshcatalogclient.DecodeModule(modules[n]); err == nil {
tm.FP, tm.None = m.Fp2, m.None2
}
if len(out.Tiers) == 0 {
out.Tiers = append(out.Tiers, nil)
@@ -250,11 +255,11 @@ func (r Reader) away() map[string]bool {
return out
}
for key, raw := range presence {
node, part, _, err := delivery.ParsePresenceKey(key)
node, part, _, err := meshwatchdogsclient.ParsePresenceKey(key)
if err != nil || part != "" {
continue
}
if p, err := delivery.DecodePresence(raw); err == nil && !p.Online {
if p, err := meshwatchdogsclient.DecodeNodePresence(raw); err == nil && !p.Online {
out[node] = true
}
}
@@ -263,18 +268,18 @@ func (r Reader) away() map[string]bool {
// waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want,
// its have, and each provision its graph node awaits.
func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, provided map[string]json.RawMessage,
func (r Reader) waitsOf(module string, m meshcatalog.ModulesByModuleValue, g graph.Node, wants, provided map[string]json.RawMessage,
perr error, away map[string]bool) []TierWait {
var out []TierWait
if m.FP2 == "" {
if m.Fp2 == "" {
return []TierWait{{Edge: "build", Words: "it has no fingerprint, so nothing is built: " + m.None2}}
}
if w, ok := r.buildWait(module, m.FP2); ok {
if w, ok := r.buildWait(module, m.Fp2); ok {
out = append(out, w)
}
var nodes []string
for key := range wants {
node, mod, err := delivery.ParseWantKey(key)
node, mod, err := meshwantsclient.ParseWantKey(key)
if err == nil && mod == module {
nodes = append(nodes, node)
}
@@ -284,21 +289,21 @@ func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, p
if away[node] {
continue
}
w, err := delivery.DecodeWant(wants[mustWantKey(node, module)])
w, err := meshwantsclient.DecodeWant(wants[mustWantKey(node, module)])
if err != nil || w.Removed {
continue
}
if w.FP != m.FP2 {
if w.Fp != m.Fp2 {
words := w.Words
if words == "" {
words = "the want names " + short(w.FP) + ", not " + short(m.FP2)
words = "the want names " + short(w.Fp) + ", not " + short(m.Fp2)
}
edge := graph.EdgeOfWords(w.Words)
if edge == "" {
edge = "want"
}
out = append(out, TierWait{Edge: edge, Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
} else if hw, ok := r.haveWait(node, module, m.FP2); ok {
out = append(out, TierWait{Edge: edge, Node: node, Rule: int(w.Rule), Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))})
} else if hw, ok := r.haveWait(node, module, m.Fp2); ok {
out = append(out, hw)
}
for _, a := range g.Awaits {
@@ -314,13 +319,13 @@ func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, p
}
func mustWantKey(node, module string) string {
k, _ := delivery.WantKey(node, module)
k, _ := meshwantsclient.WantKey(node, module)
return k
}
// buildWait is the wait of the build key <module>.<fp>, none when it is done.
func (r Reader) buildWait(module, fp string) (TierWait, bool) {
key, err := delivery.BuilderBuildKey(module, fp)
key, err := builds.BuilderBuildKey(module, fp)
if err != nil || r.Builds.Get == nil {
return TierWait{Edge: "build", Words: "the builder's builds cannot be read here"}, true
}
@@ -331,14 +336,14 @@ func (r Reader) buildWait(module, fp string) (TierWait, bool) {
case raw == nil:
return TierWait{Edge: "build", Words: "the builder holds no build of " + short(fp) + " yet"}, true
}
b, err := delivery.DecodeBuilderBuild(raw)
b, err := builds.DecodeBuilderBuild(raw)
if err != nil {
return TierWait{Edge: "build", Words: "the build key " + key + " does not read: " + err.Error()}, true
}
switch b.State {
case delivery.BuildDone:
case builds.BuildDone:
return TierWait{}, false
case delivery.BuildFailed:
case builds.BuildFailed:
return TierWait{Edge: "build", Words: "the build of " + short(fp) + " failed: " + b.Error}, true
}
words := "the build of " + short(fp) + " is " + b.State
@@ -353,7 +358,7 @@ func (r Reader) buildWait(module, fp string) (TierWait, bool) {
// haveWait is the wait of node's have of module at fp, none when it runs fp healthy.
func (r Reader) haveWait(node, module, fp string) (TierWait, bool) {
key, err := delivery.HaveKey(node, module)
key, err := nodeengineclient.HaveKey(node, module)
if err != nil || r.HavesState.Get == nil {
return TierWait{}, false
}
@@ -361,14 +366,14 @@ func (r Reader) haveWait(node, module, fp string) (TierWait, bool) {
if err != nil || raw == nil {
return TierWait{Edge: "have", Node: node, Words: node + " has not yet said what it runs of " + module}, true
}
h, err := delivery.DecodeHave(raw)
h, err := nodeengineclient.DecodeHave(raw)
if err != nil {
return TierWait{Edge: "have", Node: node, Words: "its have does not read: " + err.Error()}, true
}
if h.FP == fp && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK {
if h.Fp == fp && h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK {
return TierWait{}, false
}
words := fmt.Sprintf("%s runs %s, %s", node, short(h.FP), h.Phase)
words := fmt.Sprintf("%s runs %s, %s", node, short(h.Fp), h.Phase)
if h.Health != "" {
words += ", health " + string(h.Health)
}
@@ -19,8 +19,10 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/sdk/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// BuildUsageTool is the view's tool of the build nodes' use.
@@ -29,11 +31,11 @@ const BuildUsageTool = "build_usage"
// endedStale says why a step's end was written by its build module for a key left running (mesh-sdk buildstep's ending
// of stale steps): a restart's, or the step bound's; "" for a step that ran to its own end. Such an end is the moment the
// module noticed, not the step's, so it is counted apart and in neither the medians nor the time at capacity.
func endedStale(st delivery.Step) string {
if st.State != delivery.StepFailed {
func endedStale(st buildstep.Step) string {
if st.State != buildstep.StepFailed {
return ""
}
if st.Reason == buildstep.RestartReason || strings.HasPrefix(st.Reason, "past the step bound") {
if st.Reason == buildrun.RestartReason || strings.HasPrefix(st.Reason, "past the step bound") {
return st.Reason
}
return ""
@@ -133,7 +135,7 @@ type SeatUsage struct {
// usageStep is a step read, with the kind it is counted under.
type usageStep struct {
step delivery.Step
step buildstep.Step
kind string
}
@@ -186,12 +188,12 @@ func stepIDTime(id string) (time.Time, bool) {
}
// stepKind is the kind a step is counted under.
func stepKind(s delivery.Step, tries map[string]bool) string {
func stepKind(s buildstep.Step, tries map[string]bool) string {
kind, ask := strings.CutSuffix(s.Tool, " ask")
if !ask {
return KindHand
}
if kind == delivery.AskCheck && tries[s.ID] {
if kind == buildstep.AskCheck && tries[s.ID] {
return KindTry
}
return kind
@@ -215,15 +217,15 @@ func (u UsageReader) Usage() UsageAnswer {
if s := seats[node][lang]; s != nil {
return s
}
name, _ := delivery.BuildSeat(lang)
module, _ := delivery.BuildModuleOf(lang)
name, _ := buildstep.BuildSeat(lang)
module, _ := buildstep.BuildModuleOf(lang)
s := &SeatUsage{Seat: name, Module: module, Running: RunningUsage{ByKind: map[string]int{}}, LeftRunning: []StepUsage{}, EndedStale: []StepUsage{}, Steps: []StepUsage{}}
seats[node][lang] = s
return s
}
// Which node runs which build module: the have, <node>.mesh-build-<language>.
haves := map[string]delivery.Have{}
haves := map[string]nodeengine.HaveByNodeModuleValue{}
if u.Haves.Keys == nil || u.Haves.Get == nil {
note("the node-engine's have is not readable here, so no seat is said held")
} else if keys, err := u.Haves.Keys(); err != nil {
@@ -231,8 +233,8 @@ func (u UsageReader) Usage() UsageAnswer {
} else {
var want []string
for _, k := range keys {
for _, lang := range delivery.AskLanguages {
if m, _ := delivery.BuildModuleOf(lang); strings.HasSuffix(k, "."+m) {
for _, lang := range buildstep.AskLanguages {
if m, _ := buildstep.BuildModuleOf(lang); strings.HasSuffix(k, "."+m) {
want = append(want, k)
}
}
@@ -243,7 +245,7 @@ func (u UsageReader) Usage() UsageAnswer {
}
for i, k := range want {
if i < len(raws) && len(raws[i]) > 0 {
h, err := delivery.DecodeHave(raws[i])
h, err := nodeengineclient.DecodeHave(raws[i])
if err != nil {
note("the have %s cannot be read, so its seat is not said held: %v", k, err)
continue
@@ -256,7 +258,7 @@ func (u UsageReader) Usage() UsageAnswer {
node, module, _ := strings.Cut(k, ".")
lang := strings.TrimPrefix(module, "mesh-build-")
s := seat(node, lang)
s.Phase, s.Holds = string(h.Phase), h.Phase == delivery.PhaseRunning
s.Phase, s.Holds = string(h.Phase), h.Phase == nodeengineclient.PhaseRunning
}
// Capacity: mesh-config's resolved value of the module's `capacity` on each node.
@@ -317,8 +319,8 @@ func (u UsageReader) Usage() UsageAnswer {
}
// The steps of each build module.
steps := map[string]map[string][]usageStep{} // node -> language -> steps
for _, lang := range delivery.AskLanguages {
module, _ := delivery.BuildModuleOf(lang)
for _, lang := range buildstep.AskLanguages {
module, _ := buildstep.BuildModuleOf(lang)
sr, ok := u.Steps[lang]
if !ok || sr.Keys == nil || sr.Get == nil {
note("the steps of %s are not readable here", module)
@@ -337,11 +339,11 @@ func (u UsageReader) Usage() UsageAnswer {
}
var recent []keyAt
for _, k := range keys {
_, id, err := delivery.ParseStepKey(k)
_, id, err := buildstep.ParseStepKey(k)
if err != nil {
continue
}
if at, ok := stepIDTime(id); ok && now.Sub(at) <= 24*time.Hour+delivery.StepBound {
if at, ok := stepIDTime(id); ok && now.Sub(at) <= 24*time.Hour+buildstep.StepBound {
recent = append(recent, keyAt{k, at})
}
}
@@ -362,7 +364,7 @@ func (u UsageReader) Usage() UsageAnswer {
if i >= len(raws) || len(raws[i]) == 0 {
continue
}
s, err := delivery.DecodeStep(raws[i])
s, err := buildstep.DecodeStep(raws[i])
if err != nil {
continue
}
@@ -376,9 +378,9 @@ func (u UsageReader) Usage() UsageAnswer {
// Pauses: paused.<node> of each module; and capacity.<node>, the capacity the module says it judges by.
paused := map[string]bool{} // <node>.<language>
judged := map[string]delivery.Capacity{}
for _, lang := range delivery.AskLanguages {
module, _ := delivery.BuildModuleOf(lang)
judged := map[string]buildstep.Capacity{}
for _, lang := range buildstep.AskLanguages {
module, _ := buildstep.BuildModuleOf(lang)
pr, ok := u.Pauses[lang]
if !ok || pr.Get == nil {
note("the pauses of %s are not readable here, so none is said paused", module)
@@ -388,7 +390,7 @@ func (u UsageReader) Usage() UsageAnswer {
if seats[node][lang] == nil {
continue
}
key, err := delivery.PauseKey(node)
key, err := buildstep.PauseKey(node)
if err != nil {
continue
}
@@ -398,13 +400,13 @@ func (u UsageReader) Usage() UsageAnswer {
continue
}
if len(raw) > 0 {
if p, err := delivery.DecodePause(raw); err == nil {
if p, err := buildstep.DecodePause(raw); err == nil {
paused[node+"."+lang] = p.Paused
}
}
if key, err := delivery.CapacityKey(node); err == nil {
if key, err := buildstep.CapacityKey(node); err == nil {
if raw, err := pr.Get(key); err == nil && len(raw) > 0 {
if c, err := delivery.DecodeCapacity(raw); err == nil {
if c, err := buildstep.DecodeCapacity(raw); err == nil {
judged[node+"."+lang] = c
}
}
@@ -420,7 +422,7 @@ func (u UsageReader) Usage() UsageAnswer {
for _, node := range nodes {
nu := NodeUsage{Node: node, Seats: []SeatUsage{}, Said: []string{},
CPU: "not on the bus: the disk-load module keeps no state; " + node + "/disk-load.cpu_top reads it (the build modules' share among its units)"}
for _, lang := range delivery.AskLanguages {
for _, lang := range buildstep.AskLanguages {
s := seats[node][lang]
if s == nil {
continue
@@ -552,7 +554,7 @@ func (u UsageReader) tries(now time.Time, ans *UsageAnswer, note func(string, ..
return asks
}
// fill counts a seat's steps: running now, left running (started past delivery.StepBound, or its module no longer running on
// fill counts a seat's steps: running now, left running (started past buildstep.StepBound, or its module no longer running on
// the node: no step behind the key runs any more), and the two windows.
func (s *SeatUsage) fill(steps []usageStep, now time.Time) {
sort.Slice(steps, func(i, j int) bool { return steps[i].step.Started.Before(steps[j].step.Started) })
@@ -563,7 +565,7 @@ func (s *SeatUsage) fill(steps []usageStep, now time.Time) {
s.EndedStale = append(s.EndedStale, StepUsage{ID: st.ID, Kind: us.kind, Tool: st.Tool, Started: st.Started, Why: why})
continue
}
if st.State != delivery.StepRunning {
if st.State != buildstep.StepRunning {
live = append(live, us)
continue
}
@@ -571,8 +573,8 @@ func (s *SeatUsage) fill(steps []usageStep, now time.Time) {
switch {
case !s.Holds:
line.Why = "its key says running, and " + s.Module + " is not running on its node, so no step behind it runs"
case now.Sub(st.Started) > delivery.StepBound:
line.Why = fmt.Sprintf("its key says running since %s, past the step bound of %s: its module stopped or restarted before it wrote the end, and no asker waits for it", st.Started.Format(time.RFC3339), delivery.StepBound)
case now.Sub(st.Started) > buildstep.StepBound:
line.Why = fmt.Sprintf("its key says running since %s, past the step bound of %s: its module stopped or restarted before it wrote the end, and no asker waits for it", st.Started.Format(time.RFC3339), buildstep.StepBound)
}
if line.Why != "" {
s.LeftRunning = append(s.LeftRunning, line)
@@ -633,7 +635,7 @@ func window(steps []usageStep, now time.Time, span time.Duration, capacity int)
}
if !st.Ended.IsZero() && !st.Ended.Before(from) {
w.Finished++
if st.State == delivery.StepFailed || st.State == delivery.StepRefused {
if st.State == buildstep.StepFailed || st.State == buildstep.StepRefused {
w.Failed++
}
secs := int64(st.Ended.Sub(st.Started).Seconds())
+168 -169
View File
@@ -11,9 +11,24 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
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"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-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"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/buildrecord"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/motion"
)
// Entry is one change of a bucket's key, as the watch delivers it.
@@ -220,51 +235,50 @@ 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
placed *delivery.Placed // the seat's placement (the assigner's state, ADR 0567, ADR 0549 §3)
builds map[string]buildEntry // by <module>.build.<id>
groups map[string]groupEntry // the indexer's candidates, by group
modules map[string]moduleEntry // the indexer's modules, by module
keys map[string]keyEntry // the builder's build keys, by <module>.<fingerprint>
asks map[string]askRecord // the gate's asks, by key
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
source *sourceEntry // the gate's source key
placed *meshmoduleassignerclient.Placed // the seat's placement (the assigner's state, ADR 0567, ADR 0549 §3)
builds map[string]buildEntry // by <module>.build.<id>
groups map[string]groupEntry // the indexer's candidates, by group
modules map[string]moduleEntry // the indexer's modules, by module
keys map[string]keyEntry // the builder's build keys, by <module>.<fingerprint>
asks map[string]askRecord // the gate's asks, by key
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
source *sourceEntry // the gate's source key
deliveredEvent func(DeliveredEvent) error
motionEvent func(MotionEvent) error
told map[string]bool // the motion events said, by their ids
initial map[string]bool // the buckets whose watch handed over what stood at its start
said uint64 // the source revision whose delivery was said
composed map[string]composedEntry // the controller's digests and judgements, by <node>.<module>.<hex>
aasks map[string]assignerAskRecord // the assigner's asks (hold, restore), by key
chosen map[string]delivery.ViewAnswer // the answers the view wrote, by ask key
told map[string]bool // the motion events said, by their ids
initial map[string]bool // the buckets whose watch handed over what stood at its start
said uint64 // the source revision whose delivery was said
aasks map[string]assignerAskRecord // the assigner's asks (hold, restore), by key
chosen map[string]viewstate.ViewAnswer // the answers the view wrote, by ask key
writer Writer
backlog *Backlog
answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable)
answerOf map[string]delivery.Answer // by key: the teller's answer whole, when readable (holds.go)
holds map[string]heldEntry // the assigner's holds, by held key (holds.go)
newest map[string]string // module -> id
answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable)
answerOf map[string]meshdeliveryclient.Answer // by key: the teller's answer whole, when readable (holds.go)
holds map[string]heldEntry // the assigner's holds, by held key (holds.go)
newest map[string]string // module -> id
haves map[string]haveEntry
nodes map[string]nodeEntry
// the watchman's buckets, by key
presence map[string]presenceEntry
stuck map[string]stuckEntry
// how far each repository reached (reach.go, ADR 0556)
modHist map[string][]delivery.Module // the indexer's values of each module, oldest first
srcHist map[uint64]map[string]string // the source's heads, by revision
taken map[string]delivery.Taken // the indexer's taken, by repository key
reach map[string]delivery.Reach // the reach derived last, by repository key
reachSaid map[string]uint64 // the reached head's source revision said last, by repository key
reachNotYet map[string]string // the merges said not yet on the machines with it, by repository key
modHist map[string][]meshcatalog.ModulesByModuleValue // the indexer's values of each module, oldest first
srcHist map[uint64]map[string]string // the source's heads, by revision
taken map[string]takein.Taken // the indexer's taken, by repository key
reach map[string]viewstate.Reach // the reach derived last, by repository key
reachSaid map[string]uint64 // the reached head's source revision said last, by repository key
reachNotYet map[string]string // the merges said not yet on the machines with it, by repository key
reachPut func(ReachWrite) error
}
type presenceEntry struct {
value delivery.Presence
value meshwatchdogsclient.NodePresence
err string
at time.Time
}
type stuckEntry struct {
value delivery.Stuck
value meshwatchdogsclient.Stuck
err string
}
@@ -299,30 +313,25 @@ func (b build) done() bool { return b.State == "" && b.Failed == "" }
// fingerprint, the ref, the time), decoded as the controller writes them.
type buildEntry struct {
build
rec delivery.BuildRecord
rec buildrecord.BuildRecord
}
type groupEntry struct {
value delivery.Candidate
value meshcatalog.CandidatesByGroupValue
err string
}
type keyEntry struct {
value delivery.BuilderBuild
value builds.BuilderBuild
err string
}
type wantEntry struct {
value delivery.Want
value meshwantsclient.Want
err string
at time.Time
}
type composedEntry struct {
value delivery.ComposedDigest
err string
}
type sourceEntry struct {
revision uint64
group string
@@ -336,35 +345,35 @@ type assignerAskRecord struct {
}
type moduleEntry struct {
value delivery.Module
value meshcatalog.ModulesByModuleValue
err string
}
type askRecord struct {
value delivery.Ask
value meshdelivery.AsksByMergeGroupHeadsValue
ask oc.Ask // the router's ask the gate's value carries
err string
}
type haveEntry struct {
have delivery.Have
have nodeengine.HaveByNodeModuleValue
err string
at time.Time
}
type nodeEntry struct {
node delivery.HaveNode
node nodeengine.HaveByNodeNodeValue
err string
at time.Time
}
// New is an empty model.
func New() *Model {
return &Model{placed: delivery.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, composed: map[string]composedEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]delivery.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
return &Model{placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, 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][]delivery.Module{}, srcHist: map[uint64]map[string]string{}, taken: map[string]delivery.Taken{},
reach: map[string]delivery.Reach{}, reachSaid: map[string]uint64{}, reachNotYet: map[string]string{}}
modHist: map[string][]meshcatalog.ModulesByModuleValue{}, srcHist: map[uint64]map[string]string{}, taken: map[string]takein.Taken{},
reach: map[string]viewstate.Reach{}, reachSaid: map[string]uint64{}, reachNotYet: map[string]string{}}
}
// Apply takes one change of a key. A value it cannot read is kept as an error on its row, never guessed. When the change makes
@@ -433,13 +442,13 @@ func (m *Model) sendReach(put func(ReachWrite) error, say func(DeliveredEvent) e
m.mu.Lock()
defer m.mu.Unlock()
for _, w := range unwritten {
r, err := delivery.DecodeReach(w.Value)
if prev, ok := m.reach[w.Key]; ok && err == nil && delivery.SameReach(prev, r) {
r, err := viewstate.DecodeReach(w.Value)
if prev, ok := m.reach[w.Key]; ok && err == nil && viewstate.SameReach(prev, r) {
delete(m.reach, w.Key) // a newer value derived since stands as it is: its own write says whether it landed
}
}
for _, ev := range unsaid {
key, err := delivery.RepositoryKey(ev.Repository)
key, err := trunk.RepositoryKey(ev.Repository)
if err == nil && m.reachSaid[key] == ev.Revision && m.reachNotYet[key] == strings.Join(ev.NotYet, ",") {
m.reachNotYet[key] = unsaidMark
}
@@ -498,8 +507,8 @@ func (m *Model) deliveredNow() *DeliveredEvent {
func (m *Model) apply(e Entry) {
switch e.Bucket {
case delivery.HaveBucket:
node, _, isNode, err := delivery.ParseHaveKey(e.Key)
case HaveBucket:
node, _, isNode, err := nodeengineclient.ParseHaveKey(e.Key)
if err != nil {
return
}
@@ -508,7 +517,7 @@ func (m *Model) apply(e Entry) {
delete(m.nodes, node)
return
}
n, err := delivery.DecodeHaveNode(e.Value)
n, err := nodeengineclient.DecodeHaveNode(e.Value)
m.nodes[node] = nodeEntry{node: n, err: errText(err), at: e.At}
return
}
@@ -517,59 +526,62 @@ func (m *Model) apply(e Entry) {
delete(m.haves, key)
return
}
h, err := delivery.DecodeHave(e.Value)
h, err := nodeengineclient.DecodeHave(e.Value)
m.haves[key] = haveEntry{have: h, err: errText(err), at: e.At}
case delivery.PresenceBucket:
case PresenceBucket:
if e.Deleted {
delete(m.presence, e.Key)
return
}
p, err := delivery.DecodePresence(e.Value)
p, err := meshwatchdogsclient.DecodeNodePresence(e.Value)
m.presence[e.Key] = presenceEntry{value: p, err: errText(err), at: e.At}
case delivery.StuckBucket:
case StuckBucket:
if e.Deleted {
delete(m.stuck, e.Key)
return
}
st, err := delivery.DecodeStuck(e.Value)
st, err := meshwatchdogsclient.DecodeStuck(e.Value)
m.stuck[e.Key] = stuckEntry{value: st, err: errText(err)}
case delivery.PlacementBucket:
case PlacementBucket:
// A key that does not read places nothing on its node; the seat's verb placement says why.
_, _, _ = m.placed.Put(e.Key, e.Value, e.Deleted, e.At)
case delivery.CandidatesBucket:
case CandidatesBucket:
if e.Deleted {
delete(m.groups, e.Key)
return
}
c, err := delivery.DecodeCandidate(e.Value)
c, err := meshcatalog.DecodeCandidatesByGroupValue(e.Value)
m.groups[e.Key] = groupEntry{value: c, err: errText(err)}
case delivery.ModulesBucket:
case ModulesBucket:
if e.Deleted {
delete(m.modules, e.Key)
return
}
mod, err := delivery.DecodeModule(e.Value)
mod, err := meshcatalogclient.DecodeModule(e.Value)
m.modules[e.Key] = moduleEntry{value: mod, err: errText(err)}
case delivery.AsksBucket:
case AsksBucket:
if e.Deleted {
delete(m.asks, e.Key)
return
}
a, err := delivery.DecodeAsk(e.Value)
a, err := meshdeliveryclient.DecodeAsk(e.Value)
var ask oc.Ask
if err == nil {
ask, err = occ.Carried(a.Ask, delivery.AskingID(e.Key, a.Asking))
var raw []byte
if raw, err = json.Marshal(a.Ask); err == nil {
ask, err = occ.Carried(raw, meshdeliveryclient.AskingID(e.Key, int(a.Asking)))
}
}
m.asks[e.Key] = askRecord{value: a, ask: ask, err: errText(err)}
case delivery.AssignerAsksBucket:
case AssignerAsksBucket:
if e.Deleted {
delete(m.aasks, e.Key)
return
}
a, err := decodeAssignerAsk(e.Key, e.Value)
m.aasks[e.Key] = assignerAskRecord{ask: a, err: errText(err)}
case delivery.ViewAnswersBucket:
askKey, err := delivery.ParseViewAnswerKey(e.Key)
case viewstate.ViewAnswersBucket:
askKey, err := viewstate.ParseViewAnswerKey(e.Key)
if err != nil {
return
}
@@ -577,18 +589,18 @@ func (m *Model) apply(e Entry) {
delete(m.chosen, askKey)
return
}
if a, err := delivery.DecodeViewAnswer(e.Value); err == nil {
if a, err := viewstate.DecodeViewAnswer(e.Value); err == nil {
m.chosen[askKey] = a
}
case delivery.AssignerWantsBucket:
case AssignerWantsBucket:
if m.applyHeld(e) {
return
}
node, module, err := delivery.ParseWantKey(e.Key)
node, module, err := meshwantsclient.ParseWantKey(e.Key)
if err != nil {
return // the node's own key is not a module's want
}
key, kerr := delivery.HaveKey(node, module)
key, kerr := nodeengineclient.HaveKey(node, module)
if kerr != nil {
return
}
@@ -596,29 +608,29 @@ func (m *Model) apply(e Entry) {
delete(m.wants, key)
return
}
w, err := delivery.DecodeWant(e.Value)
w, err := meshwantsclient.DecodeWant(e.Value)
m.wants[key] = wantEntry{value: w, err: errText(err), at: e.At}
case delivery.TakenBucket:
case TakenBucket:
if e.Deleted {
delete(m.taken, e.Key)
return
}
if t, err := delivery.DecodeTaken(e.Value); err == nil {
if t, err := takein.DecodeTaken(e.Value); err == nil {
m.taken[e.Key] = t
} else {
delete(m.taken, e.Key)
}
case delivery.SourceBucket:
if e.Key != delivery.SourceMainKey {
case SourceBucket:
if e.Key != meshdeliveryclient.SourceMainKey {
return
}
if e.Deleted {
m.source = nil
return
}
src, err := delivery.DecodeSource(e.Value)
src, err := meshdeliveryclient.DecodeSource(e.Value)
m.source = &sourceEntry{revision: e.Revision, group: src.Group, heads: src.Heads, err: errText(err)}
case delivery.AnswersBucket:
case AnswersBucket:
if e.Deleted {
delete(m.answers, e.Key)
delete(m.answerOf, e.Key)
@@ -627,19 +639,19 @@ func (m *Model) apply(e Entry) {
// The answer's warrant is `<router id>@<time>`: it ends that asking, and only that one (a later asking of the key is
// still open). An answer that cannot be read ends none.
id := ""
if a, err := delivery.DecodeAnswer(e.Value); err == nil {
if a, err := meshdeliveryclient.DecodeAnswer(e.Value); err == nil {
id, _, _ = strings.Cut(a.Warrant, "@")
m.answerOf[e.Key] = a
} else {
delete(m.answerOf, e.Key)
}
m.answers[e.Key] = id
case delivery.BuilderBuildsBucket:
case builds.BuilderBuildsBucket:
if e.Deleted {
delete(m.keys, e.Key)
return
}
b, err := delivery.DecodeBuilderBuild(e.Value)
b, err := builds.DecodeBuilderBuild(e.Value)
if err != nil {
m.keys[e.Key] = keyEntry{err: err.Error()}
return
@@ -683,16 +695,16 @@ func (m *Model) snapshot() Snapshot {
for _, a := range m.placedPairs() {
r := Row{Node: a.Node, Module: a.Module, Status: StatusAssigned}
var waits []string
hk, herr := delivery.HaveKey(a.Node, a.Module)
hk, herr := nodeengineclient.HaveKey(a.Node, a.Module)
if herr != nil {
continue
}
h, has := m.haves[hk]
// The controller no longer writes the build of an assignment (R6): the row is the want and the have of the node.
w, wanted := m.wants[hk]
wanted = wanted && w.err == "" && !w.value.Removed && w.value.FP != ""
wanted = wanted && w.err == "" && !w.value.Removed && w.value.Fp != ""
if wanted {
r.Assigned = w.value.FP
r.Assigned = w.value.Fp
}
switch {
case !has:
@@ -702,18 +714,18 @@ func (m *Model) snapshot() Snapshot {
default:
r.Have, r.Phase, r.Health = h.have.Build, string(h.have.Phase), string(h.have.Health)
if r.Have == "" {
r.Have = h.have.FP
r.Have = h.have.Fp
}
r.Since, r.Detail = h.at, h.have.Detail
if wanted && h.have.FP != w.value.FP {
waits = append(waits, "the node has not applied "+w.value.FP)
if wanted && h.have.Fp != w.value.Fp {
waits = append(waits, "the node has not applied "+w.value.Fp)
}
}
r.Waits = strings.Join(waits, "; ")
rows = append(rows, r)
}
for key, h := range m.haves {
node, module, _, perr := delivery.ParseHaveKey(key)
node, module, _, perr := nodeengineclient.ParseHaveKey(key)
if perr != nil {
continue
}
@@ -721,14 +733,14 @@ func (m *Model) snapshot() Snapshot {
continue
}
r := Row{Node: node, Module: module, Status: StatusLeftBehind, Since: h.at, Error: h.err}
if h.err == "" && h.have.Phase == delivery.PhaseRemoved {
if h.err == "" && h.have.Phase == nodeengineclient.PhaseRemoved {
// The node-engine says it took the module away: plainly removed, not left behind.
r.Status = StatusRemoved
}
if h.err == "" {
r.Have, r.Phase, r.Health, r.Detail = h.have.Build, string(h.have.Phase), string(h.have.Health), h.have.Detail
if r.Have == "" {
r.Have = h.have.FP
r.Have = h.have.Fp
}
}
rows = append(rows, r)
@@ -741,22 +753,22 @@ func (m *Model) snapshot() Snapshot {
})
nodes := []NodeRow{}
for name, n := range m.nodes {
nodes = append(nodes, NodeRow{Node: name, Windows: count(n.node.Windows), Busy: count(n.node.Busy),
Strays: count(n.node.Strays), Since: n.at, Error: n.err})
nodes = append(nodes, NodeRow{Node: name, Windows: len(n.node.Windows), Busy: len(n.node.Busy),
Strays: len(n.node.Strays), Since: n.at, Error: n.err})
}
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Node < nodes[j].Node })
offline, stuck, unreadable := []OfflineRow{}, []StuckRow{}, []UnreadableRow{}
for key, p := range m.presence {
if p.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.PresenceBucket, Key: key, Error: p.err})
unreadable = append(unreadable, UnreadableRow{Bucket: PresenceBucket, Key: key, Error: p.err})
continue
}
if p.value.Online {
continue
}
node, part, instance, err := delivery.ParsePresenceKey(key)
node, part, instance, err := meshwatchdogsclient.ParsePresenceKey(key)
if err != nil {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.PresenceBucket, Key: key, Error: err.Error()})
unreadable = append(unreadable, UnreadableRow{Bucket: PresenceBucket, Key: key, Error: err.Error()})
continue
}
if part != "" {
@@ -767,7 +779,7 @@ func (m *Model) snapshot() Snapshot {
}
for key, st := range m.stuck {
if st.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.StuckBucket, Key: key, Error: st.err})
unreadable = append(unreadable, UnreadableRow{Bucket: StuckBucket, Key: key, Error: st.err})
continue
}
if st.value.Moved {
@@ -777,27 +789,27 @@ func (m *Model) snapshot() Snapshot {
}
for key, g := range m.groups {
if g.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.CandidatesBucket, Key: key, Error: g.err})
unreadable = append(unreadable, UnreadableRow{Bucket: CandidatesBucket, Key: key, Error: g.err})
}
}
for key, mod := range m.modules {
if mod.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.ModulesBucket, Key: key, Error: mod.err})
unreadable = append(unreadable, UnreadableRow{Bucket: ModulesBucket, Key: key, Error: mod.err})
}
}
for key, a := range m.asks {
if a.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.AsksBucket, Key: key, Error: a.err})
unreadable = append(unreadable, UnreadableRow{Bucket: AsksBucket, Key: key, Error: a.err})
}
}
for key, a := range m.aasks {
if a.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.AssignerAsksBucket, Key: key, Error: a.err})
unreadable = append(unreadable, UnreadableRow{Bucket: AssignerAsksBucket, Key: key, Error: a.err})
}
}
for key, w := range m.wants {
if w.err != "" {
unreadable = append(unreadable, UnreadableRow{Bucket: delivery.AssignerWantsBucket, Key: key, Error: w.err})
unreadable = append(unreadable, UnreadableRow{Bucket: AssignerWantsBucket, Key: key, Error: w.err})
}
}
sort.Slice(offline, func(i, j int) bool {
@@ -823,11 +835,11 @@ func (m *Model) groupRows() []GroupRow {
}
row := GroupRow{Group: key, Pulls: []GroupPull{}}
for _, p := range g.value.Pulls {
head := p.Head.Head
head := p.Head
if len(head) > 12 {
head = head[:12]
}
row.Pulls = append(row.Pulls, GroupPull{Repository: p.Repository, Number: p.Number, Head: head, Modules: append([]string{}, p.Modules...)})
row.Pulls = append(row.Pulls, GroupPull{Repository: p.Repository, Number: int(p.Number), Head: head, Modules: append([]string{}, p.Modules...)})
}
sort.Slice(row.Pulls, func(i, j int) bool {
a, b := row.Pulls[i], row.Pulls[j]
@@ -840,7 +852,7 @@ func (m *Model) groupRows() []GroupRow {
}
// fingerprintRows compare each module the indexer fingerprinted with the source fingerprint of the module's newest
// successful trunk build (delivery.BuildRecord.IsTrunkBuild: an outcome, not failed, on main or with no ref) that recorded one. The newest key is any state and ref, so it is not
// successful trunk build (buildrecord.BuildRecord.IsTrunkBuild: an outcome, not failed, on main or with no ref) that recorded one. The newest key is any state and ref, so it is not
// used: the records are.
func (m *Model) fingerprintRows() []FingerprintRow {
out := []FingerprintRow{}
@@ -849,7 +861,7 @@ func (m *Model) fingerprintRows() []FingerprintRow {
continue
}
row := FingerprintRow{Module: name, Repository: mod.value.Repository, Commit: mod.value.Commit}
if mod.value.FP2 == "" && mod.value.None2 == "" {
if mod.value.Fp2 == "" && mod.value.None2 == "" {
// The builder builds the new form alone, and its done keys are the build history the view reads (ADR 0571 §1).
row.State, row.Reason = FPNone, mod.value.None
if row.Reason == "" {
@@ -866,13 +878,13 @@ func (m *Model) fingerprintRows() []FingerprintRow {
// newFormRow is a module's row of the new form: none with its reason, or its fp2 against the state of the builder's key
// of it (I5, issue 1851; the build records name no fingerprint of the new form).
func (m *Model) newFormRow(name string, row FingerprintRow, v delivery.Module) FingerprintRow {
if v.FP2 == "" {
func (m *Model) newFormRow(name string, row FingerprintRow, v meshcatalog.ModulesByModuleValue) FingerprintRow {
if v.Fp2 == "" {
row.State, row.Reason = FPNone, v.None2
return row
}
row.Indexer = v.FP2
key, err := delivery.BuilderBuildKey(name, v.FP2)
row.Indexer = v.Fp2
key, err := builds.BuilderBuildKey(name, v.Fp2)
if err != nil {
row.State, row.Reason = FPNoKey, err.Error()
return row
@@ -883,9 +895,9 @@ func (m *Model) newFormRow(name string, row FingerprintRow, v delivery.Module) F
row.State = FPNoKey
case k.err != "":
row.State, row.Reason = FPBuildFailed, "the key cannot be read: "+k.err
case k.value.State == delivery.BuildDone:
row.State, row.Build = FPBuilt, v.FP2
case k.value.State == delivery.BuildFailed:
case k.value.State == builds.BuildDone:
row.State, row.Build = FPBuilt, v.Fp2
case k.value.State == builds.BuildFailed:
row.State, row.Reason = FPBuildFailed, k.value.Error
default:
row.State, row.Reason = FPBuilding, strings.TrimSpace(k.value.State+" "+k.value.Waits)
@@ -893,19 +905,6 @@ func (m *Model) newFormRow(name string, row FingerprintRow, v delivery.Module) F
return row
}
// count is the number of items of a carried JSON array or object, and 0 for anything else.
func count(raw json.RawMessage) int {
var list []json.RawMessage
if json.Unmarshal(raw, &list) == nil {
return len(list)
}
var obj map[string]json.RawMessage
if json.Unmarshal(raw, &obj) == nil {
return len(obj)
}
return 0
}
// Counted is the number of rows that are modules on the nodes, not left behind.
func (s Snapshot) Counted() int {
n := 0
@@ -970,11 +969,11 @@ func expiry(a oc.Ask) time.Time {
// decodeAssignerAsk reads a key of the assigner's asks (mesh-sdk go/delivery DecodeAssignerAsk): the router's ask under its
// asking's id, offering an option (occ.Carried).
func decodeAssignerAsk(key string, value []byte) (oc.Ask, error) {
a, err := delivery.DecodeAssignerAsk(key, value)
a, err := asks.DecodeAsk(key, value)
if err != nil {
return oc.Ask{}, fmt.Errorf("the assigner's ask %s is not readable: %w", key, err)
}
ask, err := occ.Carried(a.Ask, delivery.AskingID(key, a.Asking))
ask, err := occ.Carried(a.Ask, meshdeliveryclient.AskingID(key, a.Asking))
if err != nil {
return oc.Ask{}, fmt.Errorf("the assigner's ask %s is not readable: %w", key, err)
}
@@ -1005,7 +1004,7 @@ func (m *Model) SetWriter(w Writer) {
// `<ask key>.view` and nothing else; an ask that needs proof of who answered (the merge's Approve, issue 953) is refused with
// its reason, as is an ask the view does not hold open, an option the ask does not offer, and one the view answered already.
func (m *Model) Answer(askKey, option, words string) error {
key, err := delivery.ViewAnswerKey(askKey)
key, err := viewstate.ViewAnswerKey(askKey)
if err != nil {
return err
}
@@ -1034,7 +1033,7 @@ func (m *Model) Answer(askKey, option, words string) error {
if w == nil {
return fmt.Errorf("the view cannot write its answers here")
}
body, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: a.ask.ID, Option: option, Words: strings.TrimSpace(words)})
body, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: a.ask.ID, Option: option, Words: strings.TrimSpace(words)})
if err != nil {
return err
}
@@ -1050,12 +1049,12 @@ func (m *Model) wantRows() []WantRow {
}
node, module := splitKey(key)
v := w.value
row := WantRow{Node: node, Module: module, Want: v.FP, Newest: v.Newest, Rule: v.Rule, Words: v.Words}
row := WantRow{Node: node, Module: module, Want: v.Fp, Newest: v.Newest, Rule: int(v.Rule), Words: v.Words}
h, has := m.haves[key]
if has && h.err == "" {
row.Have, row.Phase, row.Health = h.have.FP, string(h.have.Phase), string(h.have.Health)
row.Have, row.Phase, row.Health = h.have.Fp, string(h.have.Phase), string(h.have.Health)
}
if has && h.err == "" && h.have.Phase == delivery.PhaseWaitingYes && !v.Removed {
if has && h.err == "" && h.have.Phase == nodeengineclient.PhaseWaitingYes && !v.Removed {
row.Judgement, row.Judged = string(h.have.Judgement), h.have.Detail
}
switch {
@@ -1063,9 +1062,9 @@ func (m *Model) wantRows() []WantRow {
row.State = WantRemoved
case v.Rule != 0 || v.Newest != "":
row.State = WantHeldBack
case row.Have == v.FP && h.have.Health == delivery.HealthFailed:
case row.Have == v.Fp && h.have.Health == nodeengineclient.HealthFailed:
row.State = WantFailed
case row.Have == v.FP && h.have.Phase == delivery.PhaseRunning && h.have.Health == delivery.HealthOK:
case row.Have == v.Fp && h.have.Phase == nodeengineclient.PhaseRunning && h.have.Health == nodeengineclient.HealthOK:
row.State = WantApplied
default:
row.State = WantPending
@@ -1091,7 +1090,7 @@ func (m *Model) sourceRow() *SourceRow {
}
waiting := []string{}
short := func(fp string) string {
fp = strings.TrimPrefix(fp, delivery.SourcePrefix2)
fp = strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
if len(fp) > 12 {
fp = fp[:12]
}
@@ -1105,31 +1104,31 @@ func (m *Model) sourceRow() *SourceRow {
continue
}
mod, ok := m.modules[module]
if w.err != "" || w.value.Removed || !ok || mod.err != "" || mod.value.FP2 == "" {
if w.err != "" || w.value.Removed || !ok || mod.err != "" || mod.value.Fp2 == "" {
continue
}
looked++
h := m.haves[key]
switch {
case w.value.FP != mod.value.FP2:
waiting = append(waiting, fmt.Sprintf("%s on %s wants %s, not the module's %s", module, node, short(w.value.FP), short(mod.value.FP2)))
case h.err != "" || h.have.FP != w.value.FP:
waiting = append(waiting, fmt.Sprintf("%s on %s does not yet run %s", module, node, short(w.value.FP)))
case h.have.Health == delivery.HealthFailed:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s and its health check failed", module, node, short(w.value.FP)))
case h.have.Phase != delivery.PhaseRunning || h.have.Health != delivery.HealthOK:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet running and healthy", module, node, short(w.value.FP)))
case w.value.Fp != mod.value.Fp2:
waiting = append(waiting, fmt.Sprintf("%s on %s wants %s, not the module's %s", module, node, short(w.value.Fp), short(mod.value.Fp2)))
case h.err != "" || h.have.Fp != w.value.Fp:
waiting = append(waiting, fmt.Sprintf("%s on %s does not yet run %s", module, node, short(w.value.Fp)))
case h.have.Health == nodeengineclient.HealthFailed:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s and its health check failed", module, node, short(w.value.Fp)))
case h.have.Phase != nodeengineclient.PhaseRunning || h.have.Health != nodeengineclient.HealthOK:
waiting = append(waiting, fmt.Sprintf("%s on %s runs %s, not yet running and healthy", module, node, short(w.value.Fp)))
}
}
for _, a := range m.placedPairs() {
wk, werr := delivery.HaveKey(a.Node, a.Module)
wk, werr := nodeengineclient.HaveKey(a.Node, a.Module)
if werr != nil || away[a.Node] {
continue
}
if _, ok := m.wants[wk]; ok {
continue
}
if mod, ok := m.modules[a.Module]; ok && mod.err == "" && mod.value.FP2 != "" {
if mod, ok := m.modules[a.Module]; ok && mod.err == "" && mod.value.Fp2 != "" {
waiting = append(waiting, fmt.Sprintf("%s on %s has no want yet", a.Module, a.Node))
}
}
@@ -1149,7 +1148,7 @@ func (m *Model) awayNodes() map[string]bool {
if p.err != "" || p.value.Online {
continue
}
if node, part, _, err := delivery.ParsePresenceKey(key); err == nil && part == "" {
if node, part, _, err := meshwatchdogsclient.ParsePresenceKey(key); err == nil && part == "" {
out[node] = true
}
}
@@ -1158,7 +1157,7 @@ func (m *Model) awayNodes() map[string]bool {
// splitKey is the node and the module (unescaped, issue 1150) of a model key, which is the have bucket's key form.
func splitKey(key string) (node, module string) {
node, module, isNode, err := delivery.ParseHaveKey(key)
node, module, isNode, err := nodeengineclient.ParseHaveKey(key)
if err != nil || isNode {
node, module, _ = strings.Cut(key, ".")
}
@@ -1169,9 +1168,9 @@ func splitKey(key string) (node, module string) {
// module's want on a node differs from its have (moving), that the have reached the want running and healthy (settled), or that it
// is held or failed or stopped moving past the watchman's bound (stuck). Each is said once for a node, a module and a composition.
const (
MotionMoving = delivery.MotionMoving
MotionSettled = delivery.MotionSettled
MotionStuck = delivery.MotionStuck
MotionMoving = motion.MotionMoving
MotionSettled = motion.MotionSettled
MotionStuck = motion.MotionStuck
// MotionBoundSeconds is how long a node may take to apply a want before the watchman says it stuck (watch.StepBound).
MotionBoundSeconds = 1800
)
@@ -1216,27 +1215,27 @@ func (m *Model) motionNow(e Entry) []MotionEvent {
}
var hk string
switch e.Bucket {
case delivery.AssignerWantsBucket:
node, module, err := delivery.ParseWantKey(e.Key)
case AssignerWantsBucket:
node, module, err := meshwantsclient.ParseWantKey(e.Key)
if err != nil {
return nil
}
hk, _ = delivery.HaveKey(node, module)
case delivery.HaveBucket:
if _, _, isNode, err := delivery.ParseHaveKey(e.Key); err != nil || isNode {
hk, _ = nodeengineclient.HaveKey(node, module)
case HaveBucket:
if _, _, isNode, err := nodeengineclient.ParseHaveKey(e.Key); err != nil || isNode {
return nil
}
hk = e.Key
case delivery.StuckBucket:
case StuckBucket:
st, ok := m.stuck[e.Key]
if !ok || st.err != "" || st.value.Bucket != delivery.AssignerWantsBucket {
if !ok || st.err != "" || st.value.Bucket != AssignerWantsBucket {
return nil
}
node, module, err := delivery.ParseWantKey(st.value.Key)
node, module, err := meshwantsclient.ParseWantKey(st.value.Key)
if err != nil {
return nil
}
hk, _ = delivery.HaveKey(node, module)
hk, _ = nodeengineclient.HaveKey(node, module)
default:
return nil
}
@@ -1246,7 +1245,7 @@ func (m *Model) motionNow(e Entry) []MotionEvent {
// motionReady says whether the view has read what stands in the buckets motion is derived from (the wants, the haves and the
// watchman's stuck keys): before, a want read ahead of its have would be said moving though the node runs it.
func (m *Model) motionReady() bool {
for _, b := range []string{delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.StuckBucket} {
for _, b := range []string{AssignerWantsBucket, HaveBucket, StuckBucket} {
if !m.initial[b] {
return false
}
@@ -1283,16 +1282,16 @@ func (m *Model) Initial(bucket string) {
// motionOf is the events to say now about the node and module of a model key, from their want, have and stuck key.
func (m *Model) motionOf(hk string) []MotionEvent {
w, ok := m.wants[hk]
if !ok || w.err != "" || w.value.Removed || w.value.FP == "" {
if !ok || w.err != "" || w.value.Removed || w.value.Fp == "" {
return nil
}
node, module := splitKey(hk)
h, has := m.haves[hk]
has = has && h.err == ""
body := MotionBody{Node: node, Module: module, ToFP: w.value.FP, Composed: w.value.Composed, Since: w.at, BoundSeconds: MotionBoundSeconds}
body := MotionBody{Node: node, Module: module, ToFP: w.value.Fp, Composed: w.value.Composed, Since: w.at, BoundSeconds: MotionBoundSeconds}
body.At = w.at
if has {
body.FromFP = h.have.FP
body.FromFP = h.have.Fp
if h.at.After(body.At) {
body.At = h.at
}
@@ -1308,10 +1307,10 @@ func (m *Model) motionOf(hk string) []MotionEvent {
m.told[id(kind)] = true
out = append(out, MotionEvent{Type: kind, ID: id(kind), Body: b})
}
// The derivation is mesh-sdk's (delivery.MotionOf, issue 1952 S7), shared with the condition keeper's hold.
// The derivation is mesh-sdk's (motion.MotionOf, issue 1952 S7), shared with the condition keeper's hold.
words, stuck := m.stuckWant(w.value, node, module)
mo := delivery.MotionOf(w.value, true, h.have, has, words, stuck)
if mo.State == delivery.MotionSettled {
mo := motionOf(w.value, true, h.have, has, words, stuck)
if mo.State == motion.MotionSettled {
if m.told[id(MotionMoving)] {
b := body
b.Outcome = mo.Outcome
@@ -1322,7 +1321,7 @@ func (m *Model) motionOf(hk string) []MotionEvent {
if !m.told[id(MotionMoving)] {
say(MotionMoving, body)
}
if mo.State == delivery.MotionStuck {
if mo.State == motion.MotionStuck {
stop := body
stop.Outcome, stop.Words = mo.Outcome, mo.Words
say(MotionStuck, stop)
@@ -1331,12 +1330,12 @@ func (m *Model) motionOf(hk string) []MotionEvent {
}
// stuckWant is the watchman's words for the want of module on node, when it says the want stopped moving and not yet moved.
func (m *Model) stuckWant(_ delivery.Want, node, module string) (string, bool) {
wk, err := delivery.WantKey(node, module)
func (m *Model) stuckWant(_ meshwantsclient.Want, node, module string) (string, bool) {
wk, err := meshwantsclient.WantKey(node, module)
if err != nil {
return "", false
}
sk, err := delivery.StuckKey(delivery.AssignerWantsBucket, wk)
sk, err := meshwatchdogsclient.StuckKey(AssignerWantsBucket, wk)
if err != nil {
return "", false
}
@@ -8,15 +8,23 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/motion"
)
var t0 = time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)
func have(t *testing.T, build string, phase delivery.Phase, health delivery.Health, detail string) []byte {
func have(t *testing.T, build string, phase string, health string, detail string) []byte {
t.Helper()
b, err := delivery.EncodeHave(delivery.Have{Build: build, Phase: phase, Health: health, Detail: detail})
b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: build, Phase: phase, Health: health, Detail: detail})
if err != nil {
t.Fatal(err)
}
@@ -24,23 +32,23 @@ func have(t *testing.T, build string, phase delivery.Phase, health delivery.Heal
}
func haveE(t *testing.T, node, module string, v []byte, at time.Time) Entry {
return Entry{Bucket: delivery.HaveBucket, Key: node + "." + module, Value: v, At: at}
return Entry{Bucket: HaveBucket, Key: node + "." + module, Value: v, At: at}
}
// place is the seat's placement of node, as the assigner keeps it (ADR 0567): the modules placed there.
func place(t *testing.T, node string, modules ...string) Entry {
t.Helper()
p := delivery.Placement{Node: node}
p := meshmoduleassigner.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
// Its node key, which names the modules placed (research 072 row 37).
values, err := delivery.PlacementValues(p)
values, err := meshmoduleassignerclient.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.PlacementNodeKey(node)
return Entry{Bucket: delivery.PlacementBucket, Key: key, Value: values[key], At: t0}
key, _ := meshmoduleassignerclient.NodeKey(node)
return Entry{Bucket: PlacementBucket, Key: key, Value: values[key], At: t0}
}
func find(t *testing.T, s Snapshot, node, module string) Row {
@@ -64,9 +72,9 @@ func fill(m *Model, es ...Entry) {
func base(t *testing.T) []Entry {
es := []Entry{
place(t, "a", "x", "y"), place(t, "b", "x", "z"),
haveE(t, "a", "x", have(t, "b1", delivery.PhaseRunning, delivery.HealthOK, ""), t0.Add(1*time.Minute)),
haveE(t, "b", "x", have(t, "b1", delivery.PhaseChecking, delivery.HealthWaits, "starting"), t0.Add(2*time.Minute)),
haveE(t, "b", "z", have(t, "b3", delivery.PhaseRunning, delivery.HealthOK, ""), t0.Add(3*time.Minute)),
haveE(t, "a", "x", have(t, "b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0.Add(1*time.Minute)),
haveE(t, "b", "x", have(t, "b1", nodeengineclient.PhaseChecking, nodeengineclient.HealthWaits, "starting"), t0.Add(2*time.Minute)),
haveE(t, "b", "z", have(t, "b3", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0.Add(3*time.Minute)),
}
return es
}
@@ -100,12 +108,12 @@ func TestLeftBehindIsWhatThePlacementDoesNotPut(t *testing.T) { // 1134/AC3, iss
}
m = New()
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", delivery.PhaseRunning, delivery.HealthOK, ""), t0)) // placed nowhere
fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0)) // placed nowhere
s = m.Snapshot()
if find(t, s, "a", "w").Status != StatusLeftBehind || s.Counted() != 4 {
t.Fatalf("a have with no placement: %+v counted %d", find(t, s, "a", "w"), s.Counted())
}
fill(m, Entry{Bucket: delivery.PlacementBucket, Key: "b.node", Deleted: true})
fill(m, Entry{Bucket: PlacementBucket, Key: "b.node", Deleted: true})
if s := m.Snapshot(); s.Counted() != 2 || find(t, s, "b", "x").Status != StatusLeftBehind {
t.Fatalf("b's placement deleted: counted %d, %+v", s.Counted(), s.Rows)
}
@@ -118,8 +126,8 @@ func TestUnknownVersionSaysSo(t *testing.T) { // 1134/AC4
fill(m, haveE(t, "a", "x", []byte(`{"v":9,"phase":"running","health":"ok"}`), t0))
s := m.Snapshot()
r := find(t, s, "a", "x")
_, sdkErr := delivery.DecodeHave([]byte(`{"v":9,"phase":"running","health":"ok"}`))
if !errors.Is(sdkErr, delivery.ErrUnknownVersion) {
_, sdkErr := nodeengineclient.DecodeHave([]byte(`{"v":9,"phase":"running","health":"ok"}`))
if !errors.Is(sdkErr, keys.ErrUnknownVersion) {
t.Fatal("test premise: the SDK must refuse version 9")
}
if r.Error != sdkErr.Error() || !strings.Contains(r.Error, "version 9") {
@@ -139,7 +147,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
m := New()
fill(m, base(t)...)
before := m.Snapshot()
fill(m, haveE(t, "b", "x", have(t, "b1", delivery.PhaseRunning, delivery.HealthOK, ""), t0.Add(9*time.Minute)))
fill(m, haveE(t, "b", "x", have(t, "b1", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0.Add(9*time.Minute)))
after := m.Snapshot()
for i := range before.Rows {
changed := !reflect.DeepEqual(before.Rows[i], after.Rows[i])
@@ -151,7 +159,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
t.Fatalf("changed row = %+v", r)
}
// a want's change touches only its row
fill(m, wantE(t, "a", "y", delivery.Want{FP: fpNew}))
fill(m, wantE(t, "a", "y", meshwantsclient.Want{Fp: fpNew}))
if r := find(t, m.Snapshot(), "a", "y"); r.Assigned != fpNew {
t.Fatalf("want change not seen: %+v", r)
}
@@ -159,7 +167,7 @@ func TestOnlyTheAffectedRowChanges(t *testing.T) { // 1134/AC5
t.Fatal("a want of another row changed b.z")
}
// a deleted key (a bucket's expiry) removes its value
m.Apply(Entry{Bucket: delivery.HaveBucket, Key: "a.x", Deleted: true})
m.Apply(Entry{Bucket: HaveBucket, Key: "a.x", Deleted: true})
if !strings.HasPrefix(find(t, m.Snapshot(), "a", "x").Waits, "no have yet") {
t.Fatal("a deleted have must read as no have")
}
@@ -175,8 +183,8 @@ 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()
v, _ := delivery.EncodeHaveNode(delivery.HaveNode{Windows: []byte(`[1,2]`), Busy: []byte(`{"a":1}`), Strays: []byte(`[]`)})
fill(m, Entry{Bucket: delivery.HaveBucket, Key: "a.node", Value: v, At: t0})
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
if len(n) != 1 || n[0].Node != "a" || n[0].Windows != 2 || n[0].Busy != 1 || n[0].Strays != 0 {
t.Fatalf("nodes = %+v", n)
@@ -185,20 +193,20 @@ func TestNodeRow(t *testing.T) { // 1134/AC1 (the node's own key gives a node ro
func presenceE(t *testing.T, key string, online bool, at time.Time) Entry {
t.Helper()
b, err := delivery.EncodePresence(delivery.Presence{Online: online})
b, err := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: online})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.PresenceBucket, Key: key, Value: b, At: at}
return Entry{Bucket: PresenceBucket, Key: key, Value: b, At: at}
}
func stuckE(t *testing.T, bucket, key, waited string, since time.Time, moved bool) Entry {
t.Helper()
b, err := delivery.EncodeStuck(delivery.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: since, Moved: moved})
b, err := meshwatchdogsclient.EncodeStuck(meshwatchdogsclient.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: since, Moved: moved})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.StuckBucket, Key: bucket + "." + key, Value: b, At: since}
return Entry{Bucket: StuckBucket, Key: bucket + "." + key, Value: b, At: since}
}
func TestTheWatchmansKeysAndStatus(t *testing.T) {
@@ -238,22 +246,22 @@ func offlineAndStuckAreListed(t *testing.T) { // 1168/AC2
func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3
m := New()
fill(m,
Entry{Bucket: delivery.PresenceBucket, Key: "c", Value: []byte(`{"v":9,"online":false}`), At: t0},
Entry{Bucket: delivery.StuckBucket, Key: "node-engine_have.c.x", Value: []byte(`{"v":9}`), At: t0},
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},
presenceE(t, "a", false, t0),
)
s := m.Snapshot()
if len(s.Unreadable) != 2 || len(s.Offline) != 1 {
t.Fatalf("unreadable %+v offline %+v", s.Unreadable, s.Offline)
}
_, sdkErr := delivery.DecodePresence([]byte(`{"v":9,"online":false}`))
_, sdkErr := meshwatchdogsclient.DecodeNodePresence([]byte(`{"v":9,"online":false}`))
var got *UnreadableRow
for i := range s.Unreadable {
if s.Unreadable[i].Key == "c" {
got = &s.Unreadable[i]
}
}
if got == nil || got.Bucket != delivery.PresenceBucket || got.Error != sdkErr.Error() {
if got == nil || got.Bucket != PresenceBucket || got.Error != sdkErr.Error() {
t.Fatalf("unreadable = %+v, want key c with the SDK's error %q", s.Unreadable, sdkErr)
}
// a good value later replaces the unreadable one
@@ -268,25 +276,28 @@ func unknownWatchmanVersionIsUnreadableNotDropped(t *testing.T) { // 1168/AC3
func statusDerivesEachPartFromItsBucket(t *testing.T) {
now := t0.Add(time.Hour)
composedC := "sha256:" + strings.Repeat("c", 64)
fp1, fp2, fp3 := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64), delivery.SourcePrefix2+strings.Repeat("3", 64)
fp1, fp2, fp3 := meshcatalogclient.SourcePrefix2+strings.Repeat("1", 64), meshcatalogclient.SourcePrefix2+strings.Repeat("2", 64), meshcatalogclient.SourcePrefix2+strings.Repeat("3", 64)
enc := func(v any) string { b, _ := json.Marshal(v); return string(b) }
want := func(fp string) string {
b, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: composedC})
b, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fp, Composed: composedC})
return string(b)
}
have := func(fp string, phase delivery.Phase, health delivery.Health, detail string) string {
b, err := delivery.EncodeHave(delivery.Have{FP: fp, Composed: composedC, Phase: phase, Health: health, Detail: detail})
have := func(fp string, phase string, health string, detail string) string {
b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: composedC, Phase: phase, Health: health, Detail: detail})
if err != nil {
t.Fatal(err)
}
return string(b)
}
presence := func(on bool) string { b, _ := delivery.EncodePresence(delivery.Presence{Online: on}); return string(b) }
stuckOf := func(bucket, key, waited string, moved bool) string {
b, _ := delivery.EncodeStuck(delivery.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: t0, Moved: moved})
presence := func(on bool) string {
b, _ := meshwatchdogsclient.EncodeNodePresence(meshwatchdogsclient.NodePresence{Online: on})
return string(b)
}
stuckWant, _ := delivery.StuckKey(delivery.AssignerWantsBucket, "want.b.web")
stuckOf := func(bucket, key, waited string, moved bool) string {
b, _ := meshwatchdogsclient.EncodeStuck(meshwatchdogsclient.Stuck{Bucket: bucket, Key: key, Waited: waited, Since: t0, Moved: moved})
return string(b)
}
stuckWant, _ := meshwatchdogsclient.StuckKey(AssignerWantsBucket, "want.b.web")
fin := t0.Add(time.Minute)
r := StatusRead{
Conditions: states(map[string]string{
@@ -294,11 +305,11 @@ func statusDerivesEachPartFromItsBucket(t *testing.T) {
"core.doctor.self-check": enc(health.Condition{Key: "core.doctor.self-check", Severity: health.Urgent, Summary: "no run", Raised: t0.Add(time.Minute)}),
}),
Presence: states(map[string]string{"a": presence(true), "b": presence(false)}),
Stuck: states(map[string]string{stuckWant: stuckOf(delivery.AssignerWantsBucket, "want.b.web", "b to run web", false),
Stuck: states(map[string]string{stuckWant: stuckOf(AssignerWantsBucket, "want.b.web", "b to run web", false),
"x.moved": stuckOf("x", "moved", "y", true)}),
Wants: states(map[string]string{"want.a.web": want(fp2), "want.a.mail": want(fp1), "want.b.web": want(fp2), "want.a.db": want(fp3)}),
Haves: states(map[string]string{"a.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.mail": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""),
"b.web": have(fp1, delivery.PhaseRunning, delivery.HealthOK, ""), "a.db": have(fp3, delivery.PhaseRunning, delivery.HealthFailed, "the unit failed")}),
Haves: states(map[string]string{"a.web": have(fp1, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), "a.mail": have(fp1, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""),
"b.web": have(fp1, nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), "a.db": have(fp3, nodeengineclient.PhaseRunning, nodeengineclient.HealthFailed, "the unit failed")}),
Attempts: states(map[string]string{"1": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: t0, Finished: &fin}),
"2": enc(health.Attempt{Outcome: health.OutcomeEscalated, Begun: t0}), "old": enc(health.Attempt{Outcome: health.OutcomeActed, Begun: now.Add(-8 * 24 * time.Hour)})}),
}
@@ -310,9 +321,9 @@ func statusDerivesEachPartFromItsBucket(t *testing.T) {
t.Errorf("quiet = %+v", st.Quiet)
}
wantMoving := []StatusMoving{
{Node: "a", Module: "db", State: delivery.MotionStuck, Outcome: delivery.OutcomeFailed, Words: "the unit failed", FromFP: fp3, ToFP: fp3},
{Node: "a", Module: "web", State: delivery.MotionMoving, FromFP: fp1, ToFP: fp2},
{Node: "b", Module: "web", State: delivery.MotionStuck, Outcome: delivery.OutcomeStopped, Words: "b to run web", FromFP: fp1, ToFP: fp2},
{Node: "a", Module: "db", State: motion.MotionStuck, Outcome: motion.OutcomeFailed, Words: "the unit failed", FromFP: fp3, ToFP: fp3},
{Node: "a", Module: "web", State: motion.MotionMoving, FromFP: fp1, ToFP: fp2},
{Node: "b", Module: "web", State: motion.MotionStuck, Outcome: motion.OutcomeStopped, Words: "b to run web", FromFP: fp1, ToFP: fp2},
}
if !reflect.DeepEqual(st.Moving, wantMoving) {
t.Errorf("moving = %+v\nwant %+v", st.Moving, wantMoving)
@@ -6,7 +6,14 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-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"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
const (
@@ -14,63 +21,63 @@ const (
fpNew = "src2:2222222222222222222222222222222222222222222222222222222222222222"
)
func wantE(t *testing.T, node, module string, w delivery.Want) Entry {
func wantE(t *testing.T, node, module string, w meshwantsclient.Want) Entry {
t.Helper()
key, err := delivery.WantKey(node, module)
key, err := meshwantsclient.WantKey(node, module)
if err != nil {
t.Fatal(err)
}
if !w.Removed {
w.Composed = "sha256:" + strings.Repeat("c", 64)
}
v, err := delivery.EncodeWant(w)
v, err := meshwantsclient.EncodeWant(w)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AssignerWantsBucket, Key: key, Value: v, At: t0}
return Entry{Bucket: AssignerWantsBucket, Key: key, Value: v, At: t0}
}
func haveFP(t *testing.T, node, module, fp string, health delivery.Health) Entry {
func haveFP(t *testing.T, node, module, fp string, health string) Entry {
t.Helper()
b, err := delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: health})
b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: health})
if err != nil {
t.Fatal(err)
}
key, err := delivery.HaveKey(node, module)
key, err := nodeengineclient.HaveKey(node, module)
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.HaveBucket, Key: key, Value: b, At: t0}
return Entry{Bucket: HaveBucket, Key: key, Value: b, At: t0}
}
func moduleE(t *testing.T, module, fp string) Entry {
t.Helper()
v, err := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: strings.Repeat("a", 64), Repository: "novox/" + module, Commit: strings.Repeat("1", 40), Path: delivery.RootPath})
v, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: strings.Repeat("a", 64), Repository: "novox/" + module, Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.ModulesBucket, Key: module, Value: v, At: t0}
return Entry{Bucket: ModulesBucket, Key: module, Value: v, At: t0}
}
func sourceE(t *testing.T, revision uint64) Entry {
t.Helper()
v, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: map[string]string{"novox/web": strings.Repeat("1", 40)}})
v, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/web": strings.Repeat("1", 40)}})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.SourceBucket, Key: delivery.SourceMainKey, Value: v, At: t0, Revision: revision}
return Entry{Bucket: SourceBucket, Key: meshdeliveryclient.SourceMainKey, Value: v, At: t0, Revision: revision}
}
// 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()
fill(m,
wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpNew, delivery.HealthOK),
wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), haveFP(t, "ace", "mail", fpOld, delivery.HealthOK),
wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthFailed),
wantE(t, "g14", "mail", delivery.Want{FP: fpOld, Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace does not run it healthy yet"}),
wantE(t, "g14", "old", delivery.Want{Removed: true}),
wantE(t, "novox", "web", delivery.Want{FP: fpNew}),
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),
wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthFailed),
wantE(t, "g14", "mail", meshwantsclient.Want{Fp: fpOld, Newest: fpNew, Rule: meshwantsclient.RuleCanary, Words: "the canary ace does not run it healthy yet"}),
wantE(t, "g14", "old", meshwantsclient.Want{Removed: true}),
wantE(t, "novox", "web", meshwantsclient.Want{Fp: fpNew}),
)
got := map[string]WantRow{}
for _, r := range m.Snapshot().Wants {
@@ -83,14 +90,14 @@ func TestEveryWantIsARowAgainstItsHave(t *testing.T) {
t.Errorf("%s = %+v, want state %s", key, got[key], state)
}
}
if r := got["g14.mail"]; r.Rule != delivery.RuleCanary || r.Newest != fpNew || r.Words == "" {
if r := got["g14.mail"]; r.Rule != meshwantsclient.RuleCanary || r.Newest != fpNew || r.Words == "" {
t.Errorf("the held-back row keeps its rule and words: %+v", r)
}
if r := got["ace.mail"]; r.Want != fpNew || r.Have != fpOld {
t.Errorf("a pending row shows both: %+v", r)
}
// a want taken away leaves the list
fill(m, Entry{Bucket: delivery.AssignerWantsBucket, Key: "want.novox.web", Deleted: true})
fill(m, Entry{Bucket: AssignerWantsBucket, Key: "want.novox.web", Deleted: true})
if n := len(m.Snapshot().Wants); n != 5 {
t.Errorf("wants = %d after a delete, want 5", n)
}
@@ -102,24 +109,24 @@ func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) {
m := New()
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), moduleE(t, "mail", fpNew),
place(t, "ace", "web"), place(t, "g14", "web"),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpNew, delivery.HealthOK),
wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthOK),
wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), haveFP(t, "ace", "mail", fpNew, delivery.HealthOK))
wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK),
wantE(t, "g14", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthOK),
wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "mail", fpNew, nodeengineclient.HealthOK))
s := m.Snapshot().Source
if s == nil || s.Revision != 14355 || s.State != SourceDelivered || len(s.Waiting) != 0 {
t.Fatalf("source = %+v, want revision 14355 delivered", s)
}
fill(m, haveFP(t, "g14", "web", fpNew, delivery.HealthFailed))
fill(m, haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthFailed))
s = m.Snapshot().Source
if s.State != SourceNotDelivered || len(s.Waiting) != 1 || !strings.Contains(s.Waiting[0], "web") || !strings.Contains(s.Waiting[0], "g14") {
t.Errorf("a failed have: %+v", s)
}
fill(m, haveFP(t, "g14", "web", fpNew, delivery.HealthOK), wantE(t, "ace", "mail", delivery.Want{FP: fpOld}))
fill(m, haveFP(t, "g14", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpOld}))
s = m.Snapshot().Source
if s.State != SourceNotDelivered || !strings.Contains(strings.Join(s.Waiting, ";"), "mail") {
t.Errorf("a want that is not the module's fingerprint: %+v", s)
}
fill(m, wantE(t, "ace", "mail", delivery.Want{FP: fpNew}), place(t, "novox", "web"))
fill(m, wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}), place(t, "novox", "web"))
s = m.Snapshot().Source
if s.State != SourceNotDelivered || !strings.Contains(strings.Join(s.Waiting, ";"), "novox") {
t.Errorf("a node assigned the module with no want: %+v", s)
@@ -144,7 +151,7 @@ func TestNoSourceRevisionHeldMeansNoStateIsDerived(t *testing.T) {
func TestTheWantsPageSaysWantsAgainstHaves(t *testing.T) {
m := New()
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
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" {
t.Errorf("text = %v", text)
@@ -156,14 +163,14 @@ 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()
h, err := delivery.EncodeHave(delivery.Have{FP: fpOld, Phase: delivery.PhaseWaitingYes, Health: delivery.HealthOK,
Judgement: delivery.JudgedCannot, Waits: "sha256:" + strings.Repeat("c", 64), Detail: "the composition it runs is gone"})
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 {
t.Fatal(err)
}
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), Entry{Bucket: delivery.HaveBucket, Key: "ace.web", Value: h, At: t0})
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), Entry{Bucket: HaveBucket, Key: "ace.web", Value: h, At: t0})
rows := m.Snapshot().Wants
if len(rows) != 1 || rows[0].Judgement != string(delivery.JudgedCannot) || !strings.Contains(rows[0].Judged, "gone") {
if len(rows) != 1 || rows[0].Judgement != string(nodeengineclient.JudgedCannot) || !strings.Contains(rows[0].Judged, "gone") {
t.Fatalf("rows = %+v", rows)
}
text := wantText(m.Snapshot())
@@ -187,23 +194,23 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
return nil
})
fill(m, sourceE(t, 14355), moduleE(t, "web", fpNew), place(t, "ace", "web"),
wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if len(said) != 0 {
t.Fatalf("said %v before any have", said)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthFailed))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthFailed))
if len(said) != 0 {
t.Fatalf("said %v for a failed have", said)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if len(said) != 0 || refuse {
t.Fatalf("said = %+v though the bus refused it", said)
}
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if len(said) != 1 || said[0].Revision != 14355 || said[0].Group != "g" || said[0].Heads["novox/web"] == "" {
t.Fatalf("said = %+v, want revision 14355 once with its group and heads", said)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK), wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if len(said) != 1 {
t.Errorf("said again for the same revision: %v", said)
}
@@ -212,7 +219,7 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
if len(said) != 2 || said[1].Revision != 14356 {
t.Errorf("the next revision: %v", said)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthFailed), haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthFailed), haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if len(said) != 2 {
t.Errorf("said again after a have failed and recovered: %v", said)
}
@@ -222,12 +229,12 @@ func TestTheViewSaysOnceThatASourceRevisionIsDelivered(t *testing.T) {
// have: the row shows the fingerprint wanted and waits while the node runs another.
func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
m := New()
fill(m, place(t, "ace", "web"), wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
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") {
t.Fatalf("row = %+v, want it to wait for the want's fingerprint", r)
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if r := find(t, m.Snapshot(), "ace", "web"); r.Waits != "" || r.Have != fpNew {
t.Errorf("row = %+v, want no wait once the have names the want's fingerprint", r)
}
@@ -237,10 +244,10 @@ func TestARowReadsTheWantWhenTheAssignmentNamesNoBuild(t *testing.T) {
// and a have nothing is assigned to keeps the module's name.
func TestADottedModuleNameIsFoundAndKept(t *testing.T) {
m := New()
// The seat's placement refuses a dotted module name (mesh-sdk delivery.Placement), so such a module is never placed:
// The seat's placement refuses a dotted module name (mesh-sdk meshmoduleassigner.Placement), 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", delivery.Want{FP: fpNew}), haveFP(t, "ace", "novox.be", fpNew, delivery.HealthOK),
haveFP(t, "ace", "old.name", fpNew, delivery.HealthOK))
fill(m, place(t, "ace"), wantE(t, "ace", "novox.be", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "novox.be", fpNew, nodeengineclient.HealthOK),
haveFP(t, "ace", "old.name", fpNew, nodeengineclient.HealthOK))
s := m.Snapshot()
if r := find(t, s, "ace", "novox.be"); r.Have != fpNew || r.Status != StatusLeftBehind {
t.Errorf("the dotted module's row = %+v", r)
@@ -259,7 +266,7 @@ func TestADottedModuleNameIsFoundAndKept(t *testing.T) {
func motionLog(m *Model) *[]MotionEvent {
var said []MotionEvent
m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil })
for _, b := range []string{delivery.AssignerWantsBucket, delivery.HaveBucket, delivery.StuckBucket} {
for _, b := range []string{AssignerWantsBucket, HaveBucket, StuckBucket} {
m.Initial(b)
}
return &said
@@ -276,11 +283,11 @@ func types(said []MotionEvent) string {
func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
m := New()
said := motionLog(m)
fill(m, haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
if len(*said) != 0 {
t.Fatalf("said %v for a have with no want", types(*said))
}
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if types(*said) != "moving" {
t.Fatalf("said %q, want moving", types(*said))
}
@@ -289,7 +296,7 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
mv.Body.Since.IsZero() || mv.Body.At.IsZero() || mv.Body.BoundSeconds != 1800 || mv.ID != "moving.ace.web."+strings.Repeat("c", 64) {
t.Errorf("moving = %+v", mv)
}
fill(m, haveFP(t, "ace", "web", fpOld, delivery.HealthOK), wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
fill(m, haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if types(*said) != "moving" {
t.Errorf("said %q after the same state again", types(*said))
}
@@ -303,15 +310,15 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
*said = append(*said, e)
return nil
})
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if types(*said) != "moving" || !refused {
t.Errorf("said %q though the bus refused it", types(*said))
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if types(*said) != "moving,settled" || (*said)[1].Body.Outcome != "running" {
t.Errorf("said %q, want settled with its outcome", types(*said))
}
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK))
if types(*said) != "moving,settled" {
t.Errorf("settled said twice: %q", types(*said))
}
@@ -320,20 +327,20 @@ func TestAModuleInMotionIsSaidMovingThenSettledOnceEach(t *testing.T) {
func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) {
m := New()
said := motionLog(m)
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
b, _ := delivery.EncodeHave(delivery.Have{FP: fpOld, Phase: delivery.PhaseHeld, Health: delivery.HealthWaits, Detail: "no space left"})
fill(m, Entry{Bucket: delivery.HaveBucket, Key: "ace.web", Value: b, At: t0})
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"})
fill(m, Entry{Bucket: HaveBucket, Key: "ace.web", Value: b, At: t0})
if types(*said) != "moving,stuck" || (*said)[1].Body.Outcome != "stopped" || (*said)[1].Body.Words != "no space left" {
t.Fatalf("said %v: %+v", types(*said), *said)
}
fill(m, Entry{Bucket: delivery.HaveBucket, Key: "ace.web", Value: b, At: t0})
fill(m, Entry{Bucket: HaveBucket, Key: "ace.web", Value: b, At: t0})
if types(*said) != "moving,stuck" {
t.Errorf("stuck said twice: %q", types(*said))
}
// failed on the wanted fingerprint
m2 := New()
said2 := motionLog(m2)
fill(m2, wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthFailed))
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" {
t.Errorf("failed: said %q", types(*said2))
}
@@ -342,13 +349,13 @@ func TestAModuleHeldOrFailedIsSaidStuckWithItsWords(t *testing.T) {
func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T) {
m := New()
said := motionLog(m)
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpOld, delivery.HealthOK))
sk, _ := delivery.StuckKey(delivery.AssignerWantsBucket, "want.ace.web")
v, err := delivery.EncodeStuck(delivery.Stuck{Bucket: delivery.AssignerWantsBucket, Key: "want.ace.web", Waited: "the node agent of ace to run web", Since: t0})
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), haveFP(t, "ace", "web", fpOld, nodeengineclient.HealthOK))
sk, _ := meshwatchdogsclient.StuckKey(AssignerWantsBucket, "want.ace.web")
v, err := meshwatchdogsclient.EncodeStuck(meshwatchdogsclient.Stuck{Bucket: AssignerWantsBucket, Key: "want.ace.web", Waited: "the node agent of ace to run web", Since: t0})
if err != nil {
t.Fatal(err)
}
fill(m, Entry{Bucket: delivery.StuckBucket, Key: sk, Value: v, At: t0})
fill(m, Entry{Bucket: StuckBucket, Key: sk, Value: v, At: t0})
if types(*said) != "moving,stuck" || (*said)[1].Body.Outcome != "stopped" {
t.Errorf("said %q: %+v", types(*said), *said)
}
@@ -357,7 +364,7 @@ func TestAWantTheWatchmanSaysStoppedMovingIsSaidStuckPastItsBound(t *testing.T)
func TestANodeThatSettlesWithoutBeingSeenMovingSaysNothing(t *testing.T) {
m := New()
said := motionLog(m)
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK), wantE(t, "ace", "web", delivery.Want{FP: fpNew}))
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}))
if len(*said) != 0 {
t.Errorf("said %v for a module that never moved", types(*said))
}
@@ -369,18 +376,18 @@ func TestMotionIsSaidOnlyOnceTheBucketsHaveBeenRead(t *testing.T) {
m := New()
var said []MotionEvent
m.SetMotionEvent(func(e MotionEvent) error { said = append(said, e); return nil })
fill(m, wantE(t, "ace", "web", delivery.Want{FP: fpNew}), wantE(t, "ace", "mail", delivery.Want{FP: fpNew}))
m.Initial(delivery.AssignerWantsBucket)
fill(m, haveFP(t, "ace", "web", fpNew, delivery.HealthOK), haveFP(t, "ace", "mail", fpOld, delivery.HealthOK))
m.Initial(delivery.HaveBucket)
fill(m, wantE(t, "ace", "web", meshwantsclient.Want{Fp: fpNew}), wantE(t, "ace", "mail", meshwantsclient.Want{Fp: fpNew}))
m.Initial(AssignerWantsBucket)
fill(m, haveFP(t, "ace", "web", fpNew, nodeengineclient.HealthOK), haveFP(t, "ace", "mail", fpOld, nodeengineclient.HealthOK))
m.Initial(HaveBucket)
if len(said) != 0 {
t.Fatalf("said %v before the stuck keys were read", types(said))
}
m.Initial(delivery.StuckBucket)
m.Initial(StuckBucket)
if len(said) != 1 || said[0].Type != MotionMoving || said[0].Body.Module != "mail" {
t.Fatalf("said %v: %+v, want only mail moving (web is already there)", types(said), said)
}
fill(m, haveFP(t, "ace", "mail", fpNew, delivery.HealthOK))
fill(m, haveFP(t, "ace", "mail", fpNew, nodeengineclient.HealthOK))
if types(said) != "moving,settled" {
t.Errorf("said %q", types(said))
}
@@ -389,16 +396,16 @@ func TestMotionIsSaidOnlyOnceTheBucketsHaveBeenRead(t *testing.T) {
// assigner_wants lists a node's own want (want.<node>.node, ADR 0539 clause 6) among the wants, and never as unreadable:
// on 2026-10-25 every node's own want was listed unreadable, though its value was what the assigner writes (issue 1945).
func TestTheWantsListANodesOwnWant(t *testing.T) {
own, err := delivery.WantNodeKey("ace")
own, err := meshwantsclient.WantNodeKey("ace")
if err != nil {
t.Fatal(err)
}
value, err := delivery.EncodeNodeWant(delivery.NodeWant{Composed: "sha256:" + strings.Repeat("d", 64)})
value, err := meshwantsclient.EncodeNodeWant(meshwantsclient.NodeWant{Composed: "sha256:" + strings.Repeat("d", 64)})
if err != nil {
t.Fatal(err)
}
moduleKey, _ := delivery.WantKey("ace", "web")
moduleValue, _ := delivery.EncodeWant(delivery.Want{FP: fpNew, Composed: "sha256:" + strings.Repeat("c", 64)})
moduleKey, _ := meshwantsclient.WantKey("ace", "web")
moduleValue, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fpNew, Composed: "sha256:" + strings.Repeat("c", 64)})
state := map[string][]byte{own: value, moduleKey: moduleValue}
r := Reader{WantsState: StateRead{
Keys: func() ([]string, error) { return []string{own, moduleKey}, nil },
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"io/fs"
"mesh-delivery-view/internal/view"
"os"
"path/filepath"
"reflect"
@@ -11,6 +12,8 @@ import (
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
// The controller's manifest reader is mesh-controller internal/catalogue (ParseManifest), an internal package of another
@@ -141,6 +144,42 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads"}; !reflect.DeepEqual(m.Tools, want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
// state it reads directly is in `reads`, one it follows is a family it consumes, and the owners' tools it asks are invoked.
t.Run("the names are the manifest's", func(t *testing.T) {
direct := map[string]string{view.SourceBucket: view.GateModule + ".source", view.AsksBucket: view.GateModule + ".asks", view.AnswersBucket: view.TellerModule + ".answers",
view.PresenceBucket: view.WatchmanModule + ".presence", view.StuckBucket: view.WatchmanModule + ".stuck", view.HaveBucket: view.EngineModule + ".have"}
for bucket, read := range direct {
if strings.ReplaceAll(read, ".", "_") != bucket {
t.Errorf("the bucket %s is not the state %s", bucket, read)
}
if !slices.Contains(m.Reads, read) {
t.Errorf("the view reads %s, its manifest's reads are %v", read, m.Reads)
}
}
if !slices.Contains(m.Reads, view.SignersRead) {
t.Errorf("the view reads %s, its manifest's reads are %v", view.SignersRead, m.Reads)
}
followed := map[string]string{view.ModulesBucket: view.IndexerModule + ".views.modules", view.CandidatesBucket: view.IndexerModule + ".views.candidates",
view.TakenBucket: view.IndexerModule + ".views.taken", view.PlacementBucket: view.AssignerModule + ".views.placement",
view.AssignerWantsBucket: view.AssignerModule + ".views.wants", view.AssignerAsksBucket: view.AssignerModule + ".views.asks"}
for bucket, family := range followed {
if module, state, _ := strings.Cut(family, ".views."); bucket != module+"_"+state {
t.Errorf("the bucket %s is not the state %s of %s", bucket, state, module)
}
if !slices.Contains(m.Consumes, family) {
t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes)
}
}
for _, tool := range []string{view.IndexerModule + ".indexer_read_modules", view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants"} {
if !slices.Contains(m.Invokes, tool) {
t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes)
}
}
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)
}
})
for _, a := range m.Build.Artifacts {
// A bundle compiled to a binary is one executable: what the runtime loads is that binary's name, or the controller's
// module check refuses the manifest (mesh-sdk catalogue build.go, ADR 0193).