Review round 1 of the bus want: a removal keeps the bus server, the have says a server made anew runs the want's fingerprint, the old build is read before the first write, and an unanswered inspect fails the apply rather than replacing the server (ADR 0594 §2, §4)
mesh/delivery-group group feat/774-bus-want rejected: composed together: the checker's verdict at the members' heads (a0ab6ce2e316): novox
mesh/delivery closed: the pull request closed unmerged
mesh/check failed: novox: not composed on novox: nats has not been built for this mesh: server is its "server" artifact… | 2c5f818d1522 on 798d73eb11e9

This commit is contained in:
2026-11-03 09:18:31 +01:00
parent a096abac92
commit 2c5f818d15
7 changed files with 279 additions and 55 deletions
+65 -10
View File
@@ -65,10 +65,11 @@ func (f *feed) put(key string, value []byte) {
}
type haves struct {
mu sync.Mutex
stored map[string]Have
log []Have
removed []string
mu sync.Mutex
stored map[string]Have
log []Have
removed []string
removedWords []string
// unreadable is each module whose have the bus cannot read now (ReadErr).
unreadable map[string]bool
}
@@ -99,10 +100,11 @@ func (h *haves) Write(_ context.Context, x Have) {
h.stored[x.Module] = x
h.log = append(h.log, x)
}
func (h *haves) Removed(_ context.Context, m string) {
func (h *haves) Removed(_ context.Context, m string, words ...string) {
h.mu.Lock()
defer h.mu.Unlock()
h.removed = append(h.removed, m)
h.removedWords = append(h.removedWords, strings.Join(words, ""))
}
func (h *haves) last(module string) Have {
h.mu.Lock()
@@ -127,7 +129,9 @@ type bed struct {
kept map[string][]json.RawMessage
others [][]delivery.Composed
keeps []map[string]bool // what each apply was told to keep as it runs (ADR 0594 §2)
said []string
// serverGone is a bus server not on the machine: an apply keeping it keeps nothing and makes it.
serverGone bool
said []string
// settled is each module Settled was told, and settledRan whether what runs held it then.
settled []string
settledRan []bool
@@ -143,6 +147,13 @@ func newBed(t *testing.T) *bed {
defer b.mu.Unlock()
b.others = append(b.others, others)
b.keeps = append(b.keeps, keep)
var kept []string // what of keep was there to keep: all of it, unless the test took the server away
for id := range keep {
if !b.serverGone {
kept = append(kept, id)
}
}
slices.Sort(kept)
name := c.Module
if name == "" {
name = NodeModule
@@ -151,13 +162,19 @@ func newBed(t *testing.T) *bed {
if b.failWith != "" && (b.failTimes == 0 || len(b.applied) <= b.failTimes) {
return Result{Failed: true, Transient: b.transient, Detail: b.failWith}
}
return Result{}
return Result{Kept: kept}
},
Remove: func(_ context.Context, m string, _ []delivery.Composed) Result {
Remove: func(_ context.Context, m string, _ []delivery.Composed, keep map[string]bool) Result {
b.mu.Lock()
defer b.mu.Unlock()
b.removed = append(b.removed, m)
return Result{}
b.keeps = append(b.keeps, keep)
var kept []string
for id := range keep {
kept = append(kept, id)
}
slices.Sort(kept)
return Result{Kept: kept}
},
Backup: func(_ context.Context, m string) Backup {
b.mu.Lock()
@@ -1582,8 +1599,22 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
return b.keeps[len(b.keeps)-1]
}
fpA, fpB := fpOf("a"), fpOf("b")
// A have written before the first apply (the node cannot compose yet) keeps the old build's name as what the
// server runs: the writer names no build, so it is read before the first write.
want("nats", fpA, "")
eventually(t, "the bus's want was not taken", func() bool {
b.ag.mu.Lock()
defer b.ag.mu.Unlock()
return b.ag.wants["nats"].Present
})
b.ag.NotComposed(context.Background(), "a stand-in for the bus's secret")
if h := b.haves.last("nats"); h.Build != "" || h.Server != took {
t.Fatalf("a have written before the first apply: %+v", h)
}
b.ag.NotComposed(context.Background(), "")
for i, row := range []struct {
name, fp, image, users, warrant string
gone bool // the server is not on the machine: made, at the want's fingerprint
applies bool
keep map[string]bool
server string
@@ -1595,7 +1626,12 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
applies: true, server: fpB},
{name: "the same warrant again", fp: fpB, image: "2", users: "u2", warrant: delivery.WarrantOperator, server: fpB},
{name: "a build after the warrant", fp: fpA, image: "3", users: "u2", applies: true, keep: server, server: fpB},
{name: "a build whose server is not there", fp: fpOf("e"), image: "4", users: "u2", gone: true, applies: true,
keep: server, server: fpOf("e")},
} {
b.mu.Lock()
b.serverGone = row.gone
b.mu.Unlock()
before := len(b.appliedList())
key, raw, dig := bus(row.fp, row.image, row.users)
b.comps.put(key, raw)
@@ -1645,9 +1681,28 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
}
}
time.Sleep(100 * time.Millisecond)
if got := b.appliedList(); len(got) != 7 {
if got := b.appliedList(); len(got) != 8 {
t.Errorf("applied %v, not once for each change", got)
}
// A removed want removes the rest of the bus and never its server: it is kept, said in the journal and in the
// removed have's words.
rkey, _ := delivery.WantKey(node, "nats")
rraw, _ := delivery.EncodeWant(delivery.Want{Removed: true})
b.wants.put(rkey, rraw)
eventually(t, "the bus's removal was not asked", func() bool {
b.haves.mu.Lock()
defer b.haves.mu.Unlock()
return slices.Contains(b.haves.removed, "nats")
})
if got := lastKeep(); !maps.Equal(got, server) {
t.Errorf("the bus's removal kept %v, not its server %v", got, server)
}
b.haves.mu.Lock()
words := b.haves.removedWords[len(b.haves.removedWords)-1]
b.haves.mu.Unlock()
if !strings.Contains(words, "the bus server stays") || !strings.Contains(words, "nats.server") {
t.Errorf("the removed have does not say the server stays: %q", words)
}
})
}