Files
mesh-delivery/modules/mesh-delivery-view/internal/view/reach_test.go
T
jschoubben 726bbf1f1f
mesh/delivery merged: source.main moved to novox/mesh-delivery@2ac146078f47
mesh/check being carried to 726bbf1f1ffa on 2ac146078f47 | from e753009fbc71
The view writes a reach or says an event again when the bus refused it, so a refused write never leaves the reach state at an older have (issue 1126)
The view remembered a reach value, a reached head and a motion event as written or said before the bus took them; a write refused at its start (no responders) was never made again while the derivation stayed the same, and mesh-delivery.show said mesh-delivery-view not running on shanks while node_haves said it running.

Quick fix: forget only the refused reach write. Thorough fix, chosen: every write and event the view makes after its lock (reach, reached head, delivered revision, motion) answers its error, and the model forgets what was refused, so the next change it takes in makes it again; one place, the writer of the state both mesh-delivery.show and knowledge-base.progress read.
2026-10-28 16:11:52 +01:00

180 lines
8.1 KiB
Go

package view
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 1280 (ADR 0556 clauses 2 and 3): the view derives, per repository, how far each module reached and the reached head, writes
// it when it differs, and says each new reached head once as its event delivered.
func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) {
const web, sdk = "novox/web", "novox/sdk"
commitA, commitB, commitC := strings.Repeat("a", 40), strings.Repeat("b", 40), strings.Repeat("c", 40)
commitS, commitX := strings.Repeat("5", 40), strings.Repeat("e", 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})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.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}})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.SourceBucket, Key: delivery.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})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.TakenBucket, Key: key, Value: v, At: t0}
}
haveIn := func(node, module, fp string, phase delivery.Phase, health delivery.Health) Entry {
e := haveFP(t, node, module, fp, health)
v, _ := delivery.EncodeHave(delivery.Have{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)
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)}
}
for _, tc := range []struct {
name string
then []Entry
reached string // web's reached head; "" none
holds string // words one of web's holds carries; "" none
waiting string // words web's module web waits with; "" none
notYet []string // the merges the reached head's event says are not yet on the machines
refused bool // the bus refuses every write and event until the watches handed over (a start, issue 1126)
}{
{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"},
{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)}},
{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)}},
{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)}},
{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)}},
{name: "a module no node wants holds nothing", reached: commitC,
then: []Entry{wantE(t, "ace", "mail", delivery.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,
then: []Entry{mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, true), mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, false)}},
// 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)}},
} {
t.Run(tc.name, func(t *testing.T) {
m := New()
var written []ReachWrite
var said []DeliveredEvent
refuse := tc.refused
errRefused := errors.New("nats: no responders available for request")
m.SetReachWriter(func(w ReachWrite) error {
if refuse {
return errRefused
}
written = append(written, w)
return nil
})
m.SetDeliveredEvent(func(e DeliveredEvent) error {
if refuse {
return errRefused
}
if e.Repository != "" {
said = append(said, e)
}
return nil
})
fill(m, world(t)...)
if len(written) != 0 {
t.Fatalf("written before every watch handed over: %v", written)
}
for _, b := range reachBuckets {
m.Initial(b)
}
refuse = false
fill(m, tc.then...)
key, _ := delivery.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)
}
if got := strings.Join(r.Holds, "; "); (tc.holds == "") != (got == "") || !strings.Contains(got, tc.holds) {
t.Errorf("holds = %q, want words with %q", got, tc.holds)
}
if got := r.Modules["web"].Waiting; (tc.waiting == "") != (got == "") || !strings.Contains(got, tc.waiting) {
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)
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{}
for _, w := range written {
v, err := delivery.DecodeReach(w.Value)
if err != nil {
t.Fatalf("written %s: %v", w.Key, err)
}
last[w.Key] = v
}
if !delivery.SameReach(last[key], r) {
t.Errorf("the last value written is %+v, not the reach derived %+v", last[key], r)
}
if tc.reached != "" {
var last DeliveredEvent
for _, e := range said {
if e.Repository == web {
last = e
}
}
if last.Repository == "" {
t.Errorf("the reached head of %s was never said", web)
}
if strings.Join(last.NotYet, ",") != strings.Join(tc.notYet, ",") {
t.Errorf("not yet = %v, want %v", last.NotYet, tc.notYet)
}
}
{
n := len(said)
fill(m, tc.then...)
if len(said) != n && tc.reached == commitC {
t.Errorf("said again: %+v", said[n:])
}
for _, e := range said {
if len(e.Heads) != 1 || e.Group != "" || e.Revision == 0 {
t.Errorf("a reached head said as %+v", e)
}
}
}
})
}
}