Delete the declarations' stream once every node runs the new node-engine, and assert it no more (issue 1945, B13)

A node away during the switch finds its last declaration on the stream; once every node's have names the
node-engine registered, no node reads one, so the stream and the nodes' consumers on it go, said once, and a raise
leaves them out (ADR 0539 clause 11).
This commit is contained in:
2026-10-24 17:28:10 +02:00
parent 03894e5e51
commit b4a62e9e6e
8 changed files with 246 additions and 4 deletions
+20
View File
@@ -121,3 +121,23 @@ func NodeAgentAsks(node string) ([]string, error) {
// NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a
// backup is taken (`backed-up`), from whichever module holds node-backup on its machine (restic today).
var NodeAgentHears = []string{"mesh.mod.*.event.backed-up"}
// DeclarationsStream is the stream of each node's last declaration, `mesh.node.<node>.declare` (issue 107). Release 6
// sends none (ADR 0539 clauses 5 and 11): the stream is kept, with each node's consumer, only so a node away during the
// switch finds its last declaration and with it the new node-engine; the controller deletes it once every node's have
// names the new node-engine, and from then on neither asserts it nor any node's consumer on it.
const DeclarationsStream = "NODES"
// RaiseAfterDeclarations is Raise once the declarations' stream is retired: the mesh's streams and consumers but the
// declarations' stream, and no node's consumer of a declaration.
func RaiseAfterDeclarations(r Raiser) error {
for _, s := range MeshStreams() {
if s.Name == DeclarationsStream {
continue
}
if err := r.EnsureStream(s); err != nil {
return fmt.Errorf("asserting stream %s: %w", s.Name, err)
}
}
return AssertMeshConsumers(r)
}
+34
View File
@@ -72,3 +72,37 @@ func TestANodeAgentAsksItsOwnBackupHolder(t *testing.T) {
t.Errorf("a node-engine of ace reaches another machine: %v", p.Publish)
}
}
// recordingRaiser records what a raise asserts.
type recordingRaiser struct{ streams, consumers []string }
func (r *recordingRaiser) EnsureStream(s Stream) error {
r.streams = append(r.streams, s.Name)
return nil
}
func (r *recordingRaiser) EnsureConsumer(c Consumer) error {
r.consumers = append(r.consumers, c.Stream+"/"+c.Name)
return nil
}
// 1945/AC14 (unit half): once the declarations' stream is retired, a raise asserts every stream of the mesh but it,
// and no node's consumer of a declaration; before, a raise asserts both (ADR 0539 clause 11).
func TestARaiseAfterTheDeclarationsAssertsNoneOfThem(t *testing.T) {
before := &recordingRaiser{}
if err := Raise(before, []string{"ace"}); err != nil {
t.Fatal(err)
}
if !strings.Contains(strings.Join(before.streams, " "), DeclarationsStream) || !strings.Contains(strings.Join(before.consumers, " "), "NODES/ace") {
t.Fatalf("before the retirement a raise asserts %v and %v", before.streams, before.consumers)
}
after := &recordingRaiser{}
if err := RaiseAfterDeclarations(after); err != nil {
t.Fatal(err)
}
if strings.Contains(strings.Join(after.streams, " "), DeclarationsStream) || strings.Contains(strings.Join(after.consumers, " "), "NODES/") {
t.Errorf("after the retirement a raise asserts %v and %v", after.streams, after.consumers)
}
if len(after.streams) != len(before.streams)-1 {
t.Errorf("after the retirement %d streams are asserted, before %d", len(after.streams), len(before.streams))
}
}
+2 -1
View File
@@ -201,7 +201,8 @@ func TestNoPathDeletesALogOrABucket(t *testing.T) {
strings.Contains(filepath.ToSlash(path), "internal/recordmirror/")
// And the buckets of the plans' state release 6 deleted, at the controller's first start, each said (novox/hq
// ADR 0539 clause 13): the one decided deletion of a bucket.
release6 := strings.HasSuffix(filepath.ToSlash(path), "cmd/mesh-controller/composed_start.go") && name == "DeleteKeyValue"
release6 := strings.HasSuffix(filepath.ToSlash(path), "cmd/mesh-controller/composed_start.go") &&
(name == "DeleteKeyValue" || name == "DeleteStream") // and the declarations' stream once no node reads one (clause 11)
if (removers[name] && !release6) || (name == "Purge" && !decided) {
found = append(found, path+": "+name)
}