Keep the files of every composition a want still names, not only each key's newest (issue 1945; the review's C1)
A node applies exactly the composition its want names (ADR 0539 clause 1), which may be a revision a newer one replaced: its files over 64 KiB stay until no want names it.
This commit is contained in:
@@ -116,8 +116,10 @@ func namedByCompositions(node string, values [][]byte) (map[string]bool, error)
|
||||
return named, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// keptCompositionsOf are the values of every composition in a node's bucket `composed-<node>` a node may still apply:
|
||||
// each key's newest revision (each module's five newest fingerprints and the node's own, ADR 0539 §4), and any older
|
||||
// revision of it whose digest a want of the node names, since the node applies exactly the composition its want names
|
||||
// (clause 1) and a want may still name a revision a newer one replaced (the review's C1). 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 {
|
||||
@@ -137,16 +139,76 @@ func keptCompositionsOf(ctx context.Context, js jetstream.JetStream, node string
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the compositions of %s: %w", node, err)
|
||||
}
|
||||
wanted, err := digestsWantedOn(ctx, js, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out [][]byte
|
||||
for _, key := range keys {
|
||||
e, err := kv.Get(ctx, key)
|
||||
history, err := kv.History(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the composition %s: %w", key, err)
|
||||
}
|
||||
out = append(out, e.Value())
|
||||
out = append(out, keptRevisions(history, wanted)...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// keptRevisions are, of one key's history oldest first, the newest value when the key is not deleted, and every older
|
||||
// value whose composition's digest is wanted.
|
||||
func keptRevisions(history []jetstream.KeyValueEntry, wanted map[string]bool) [][]byte {
|
||||
var out [][]byte
|
||||
for i, e := range history {
|
||||
if e.Operation() != jetstream.KeyValuePut {
|
||||
continue
|
||||
}
|
||||
if i == len(history)-1 {
|
||||
out = append(out, e.Value())
|
||||
continue
|
||||
}
|
||||
c, err := sdkdelivery.DecodeComposed(e.Value())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if d, err := sdkdelivery.ComposedDigestOf(c); err == nil && wanted[d] {
|
||||
out = append(out, e.Value())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// digestsWantedOn is the digest of every composition a want of the node names, its own want's among them; none when
|
||||
// the assigner's bucket is not there yet.
|
||||
func digestsWantedOn(ctx context.Context, js jetstream.JetStream, node string) (map[string]bool, error) {
|
||||
kv, err := js.KeyValue(ctx, sdkdelivery.AssignerWantsBucket)
|
||||
if errors.Is(err, jetstream.ErrBucketNotFound) {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the wants of %s: %w", node, err)
|
||||
}
|
||||
lister, err := kv.ListKeys(ctx, jetstream.MetaOnly())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the wants of %s: %w", node, err)
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for key := range lister.Keys() {
|
||||
e, err := kv.Get(ctx, key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if n, _, err := sdkdelivery.ParseWantKey(key); err == nil && n == node {
|
||||
if w, err := sdkdelivery.DecodeWant(e.Value()); err == nil && w.Composed != "" {
|
||||
out[w.Composed] = true
|
||||
}
|
||||
} else if n, err := sdkdelivery.ParseWantNodeKey(key); err == nil && n == node {
|
||||
if w, err := sdkdelivery.DecodeNodeWant(e.Value()); err == nil {
|
||||
out[w.Composed] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
// 1945 (the review's C1): the sweep keeps the files of a key's newest composition and of every older revision a want
|
||||
// of the node still names, so a node told to apply a replaced revision finds its files; an older revision no want names,
|
||||
// and a deleted key, keep nothing.
|
||||
func TestTheSweepKeepsWhatAWantStillNames(t *testing.T) {
|
||||
revision := func(content string, rev uint64, op jetstream.KeyValueOp) fakeComposedEntry {
|
||||
c := sdkdelivery.Composed{Node: "ace", Module: "m", FP: sdkdelivery.SourcePrefix2 + strings.Repeat("a", 64),
|
||||
Resources: []json.RawMessage{json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"` + content + `"}`)}}
|
||||
raw, err := sdkdelivery.EncodeComposed(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return fakeComposedEntry{key: "k", value: raw, rev: rev, op: op}
|
||||
}
|
||||
digestOfEntry := func(e fakeComposedEntry) string {
|
||||
c, _ := sdkdelivery.DecodeComposed(e.value)
|
||||
d, _ := sdkdelivery.ComposedDigestOf(c)
|
||||
return d
|
||||
}
|
||||
one, two, three := revision("one", 1, jetstream.KeyValuePut), revision("two", 2, jetstream.KeyValuePut), revision("three", 3, jetstream.KeyValuePut)
|
||||
history := []jetstream.KeyValueEntry{one, two, three}
|
||||
kept := keptRevisions(history, map[string]bool{digestOfEntry(one): true})
|
||||
if len(kept) != 2 || string(kept[0]) != string(one.value) || string(kept[1]) != string(three.value) {
|
||||
t.Errorf("kept %d revisions, want the wanted first and the newest", len(kept))
|
||||
}
|
||||
if kept := keptRevisions(history, map[string]bool{}); len(kept) != 1 {
|
||||
t.Errorf("with no want, kept %d revisions, want the newest alone", len(kept))
|
||||
}
|
||||
deleted := []jetstream.KeyValueEntry{one, fakeComposedEntry{key: "k", rev: 2, op: jetstream.KeyValueDelete}}
|
||||
if kept := keptRevisions(deleted, map[string]bool{}); len(kept) != 0 {
|
||||
t.Errorf("a deleted key kept %d revisions", len(kept))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user