Delete the push and the declaration: the send stub and node holds, link declare and its order, the sent readers, and the sweep judged by compositions (release 6, issue 1945 B8)

This commit is contained in:
2026-10-24 17:12:35 +02:00
parent 66650e315c
commit f56458da32
57 changed files with 228 additions and 2587 deletions
+56 -38
View File
@@ -9,23 +9,22 @@ import (
"strings"
"time"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// **An object is removed a day after no node's latest declaration names it** (novox/hq ADR 0486 §3, issue 1696), so a node that is
// asleep and receives its latest declaration later still finds every object that declaration names. What each node's latest
// declaration names is read from the bus itself, the last message of that node's subject of the stream NODES: nothing else
// is kept of it. When an object is first found named by none, the time is written in its own metadata (declarationUnnamed);
// **An object is removed a day after no composition of the node names it** (novox/hq ADR 0486 §3, ADR 0539 §4, issue 1696), so a
// node that is asleep and applies its wants later still finds every object they name. What a node's compositions name is read
// from its bucket `composed-<node>`, where the controller keeps each module's five newest fingerprints and the node's own: nothing
// else is kept of them. When an object is first found named by none, the time is written in its own metadata (declarationUnnamed);
// found named again, that is erased; found unnamed for a day, it is removed. Each node has a bucket of its own, judged against that
// node's latest declaration alone. Objects are immutable and regenerable from the
// controller's state, so a removal loses nothing.
// node's compositions alone. Objects are immutable and regenerable from the controller's state, so a removal loses nothing.
// declarationUnnamed is the key of an object's metadata holding when it was first found named by no node's latest declaration.
// declarationUnnamed is the key of an object's metadata holding when it was first found named by no composition of the node.
const declarationUnnamed = "unnamed-since"
// sweepEvery is how often the bucket is judged: the removal is exact to within this.
@@ -92,41 +91,62 @@ func sweepDeclarationFiles(ctx context.Context, bucket sweptBucket, named map[st
return r, nil
}
// namedByLatest is the name of every object this latest declaration of a node names. A body that cannot be read is an error, never
// "names none": an object the node still needs would be marked for removal.
func namedByLatest(node string, signed []byte) (map[string]bool, error) {
var s link.Signed
if err := json.Unmarshal(signed, &s); err != nil {
return nil, fmt.Errorf("%s's latest declaration is not readable: %w", node, err)
}
names, err := namedObjects(s.Declaration)
if err != nil {
return nil, fmt.Errorf("%s's latest declaration is not readable: %w", node, err)
}
named := make(map[string]bool, len(names))
for name := range names {
named[name] = true
// namedByCompositions is the name of every object the compositions of a node name: the files over 64 KiB each carries by
// digest (ADR 0539 §4). A composition that cannot be read is an error, never "names none": an object the node still needs
// would be marked for removal.
func namedByCompositions(node string, values [][]byte) (map[string]bool, error) {
named := map[string]bool{}
for _, value := range values {
c, err := sdkdelivery.DecodeComposed(value)
if err != nil {
return nil, fmt.Errorf("a composition of %s is not readable: %w", node, err)
}
body, err := json.Marshal(map[string]any{"resources": c.Resources})
if err != nil {
return nil, err
}
names, err := namedObjects(body)
if err != nil {
return nil, fmt.Errorf("a composition of %s is not readable: %w", node, err)
}
for name := range names {
named[name] = true
}
}
return named, nil
}
// latestDeclarations are the last signed declarations of the nodes, by node, as the stream NODES holds them. A node with none has
// no entry.
func latestDeclarations(ctx context.Context, conn *nats.Conn, nodes []string) (map[string][]byte, error) {
js, err := conn.JetStream(nats.Context(ctx))
// keptCompositionsOf are the values of every composition in a node's bucket `composed-<node>`: what the controller keeps there
// are each module's five newest fingerprints and the node's own (ADR 0539 §4). A node with no bucket has none.
func keptCompositionsOf(ctx context.Context, js jetstream.JetStream, node string) ([][]byte, error) {
bucket, err := sdkdelivery.ComposedBucketFor(node)
if err != nil {
return nil, err
}
out := map[string][]byte{}
for _, node := range nodes {
msg, err := js.GetLastMsg("NODES", link.DeclareSubject(node), nats.Context(ctx))
if errors.Is(err, nats.ErrMsgNotFound) {
kv, err := js.KeyValue(ctx, bucket)
if errors.Is(err, jetstream.ErrBucketNotFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("the compositions of %s: %w", node, err)
}
keys, err := kv.Keys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("the compositions of %s: %w", node, err)
}
var out [][]byte
for _, key := range keys {
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("the latest declaration of %s: %w", node, err)
return nil, fmt.Errorf("the composition %s: %w", key, err)
}
out[node] = msg.Data
out = append(out, e.Value())
}
return out, nil
}
@@ -259,15 +279,13 @@ func sweepNode(ctx context.Context, conn *nats.Conn, node string) (filesSweep, e
return filesSweep{}, fmt.Errorf("the bucket cannot be read: %w", err)
}
defer lockFilesOf(node)()
latest, err := latestDeclarations(working, conn, []string{node})
values, err := keptCompositionsOf(working, js, node)
if err != nil {
return filesSweep{}, err
}
named := map[string]bool{}
if body, ok := latest[node]; ok {
if named, err = namedByLatest(node, body); err != nil {
return filesSweep{}, err
}
named, err := namedByCompositions(node, values)
if err != nil {
return filesSweep{}, err
}
return sweepDeclarationFiles(working, busSweptBucket{store}, named, time.Now())
}