Give each node a bucket of its own for the files that travel by digest, and keep the sweep from removing an object a send relies on (issues 1696, 1697; review of ADR 0486 step 3)
A literal * in a published subject matches the grant's *, so in one shared bucket a node could read every node's files. Each node now has declaration-files-<node>, which its account reads by name and the controller writes; the sweep judges each bucket against that node's latest declaration, under the node's lock with the send, and an object the sweep marked is put again by the next send. Putting that fails send after send raises a condition.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
sdkdecl "git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
@@ -202,14 +203,15 @@ func TestAFileOfBytesTravelsByDigestOnItsDecodedLength(t *testing.T) {
|
||||
}
|
||||
|
||||
type memObjects struct {
|
||||
have map[string][]byte
|
||||
puts []string
|
||||
fail map[string]error
|
||||
have map[string][]byte
|
||||
marked map[string]bool
|
||||
puts []string
|
||||
fail map[string]error
|
||||
}
|
||||
|
||||
func (m *memObjects) Has(_ context.Context, name string) (bool, error) {
|
||||
func (m *memObjects) Has(_ context.Context, name string) (bool, bool, error) {
|
||||
_, ok := m.have[name]
|
||||
return ok, nil
|
||||
return ok, m.marked[name], nil
|
||||
}
|
||||
func (m *memObjects) Put(_ context.Context, name string, c []byte) error {
|
||||
if err := m.fail[name]; err != nil {
|
||||
@@ -217,6 +219,7 @@ func (m *memObjects) Put(_ context.Context, name string, c []byte) error {
|
||||
}
|
||||
m.puts = append(m.puts, name)
|
||||
m.have[name] = c
|
||||
delete(m.marked, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,6 +230,12 @@ func TestObjectsAreInTheBucketBeforeTheDeclarationIsSent(t *testing.T) { // 1696
|
||||
if err := putObjects(t.Context(), b, objects); err != nil || len(b.puts) != 1 || b.puts[0] != strings.Repeat("b", 64) {
|
||||
t.Fatalf("put %v (%v), want the one absent", b.puts, err)
|
||||
}
|
||||
// An object the sweep marked unnamed is put again, which erases the mark: a send relies on it.
|
||||
b = &memObjects{have: map[string][]byte{strings.Repeat("a", 64): []byte("one"), strings.Repeat("b", 64): []byte("two")},
|
||||
marked: map[string]bool{strings.Repeat("a", 64): true}}
|
||||
if err := putObjects(t.Context(), b, objects); err != nil || len(b.puts) != 1 || b.puts[0] != strings.Repeat("a", 64) || b.marked[strings.Repeat("a", 64)] {
|
||||
t.Fatalf("a marked object: put %v (%v) marked %v, want it put again and the mark gone", b.puts, err, b.marked)
|
||||
}
|
||||
b = &memObjects{have: map[string][]byte{}, fail: map[string]error{strings.Repeat("a", 64): errors.New("permissions violation")}}
|
||||
err := putObjects(t.Context(), b, objects)
|
||||
if err == nil || !strings.Contains(err.Error(), "aaaaaaaaaaaa") || !strings.Contains(err.Error(), "permissions violation") {
|
||||
@@ -303,10 +312,13 @@ func (b *sweepBucketFake) ClearUnnamed(_ context.Context, n string) error {
|
||||
b.objects[n] = time.Time{}
|
||||
return nil
|
||||
}
|
||||
func (b *sweepBucketFake) Delete(_ context.Context, n string) error {
|
||||
func (b *sweepBucketFake) Delete(_ context.Context, n string, markedAt time.Time) (bool, error) {
|
||||
if at, ok := b.objects[n]; !ok || !at.Equal(markedAt) {
|
||||
return false, nil // put again since it was judged: its mark is gone
|
||||
}
|
||||
delete(b.objects, n)
|
||||
b.deleted = append(b.deleted, n)
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 1696/AC6: an object a latest declaration still names is not removed, however long; one unnamed is marked at the first look and removed
|
||||
@@ -348,12 +360,15 @@ func TestTheObjectsALatestDeclarationNamesAreReadFromTheBus(t *testing.T) {
|
||||
s := sendable{fileReferences: true, Resources: []map[string]any{aFileResource("a", contentOf("a", 70<<10))}}
|
||||
body, _ := s.Body()
|
||||
msg, _ := link.Envelope(body, []byte("sig"))
|
||||
named, err := namedByLatest(map[string][]byte{"novox": msg, "ace": []byte(`{"declaration":"` + "e30=" + `","signature":""}`)})
|
||||
named, err := namedByLatest("novox", msg)
|
||||
name, _ := sdkdecl.ObjectName(sdkdecl.DigestOf([]byte(contentOf("a", 70<<10))))
|
||||
if err != nil || len(named) != 1 || !named[name] {
|
||||
t.Fatalf("%v %v", named, err)
|
||||
}
|
||||
if _, err := namedByLatest(map[string][]byte{"novox": []byte("not json")}); err == nil {
|
||||
if none, err := namedByLatest("ace", []byte(`{"declaration":"e30=","signature":""}`)); err != nil || len(none) != 0 {
|
||||
t.Fatalf("a declaration naming none: %v %v", none, err)
|
||||
}
|
||||
if _, err := namedByLatest("novox", []byte("not json")); err == nil {
|
||||
t.Fatal("an unreadable latest declaration was read as naming none")
|
||||
}
|
||||
}
|
||||
@@ -379,3 +394,76 @@ func TestTheControlNodesDeclarationInTheReferenceFormStaysUnderHalfTheBusLimit(t
|
||||
}
|
||||
|
||||
func stdBase64(b []byte) string { return base64.StdEncoding.EncodeToString(b) }
|
||||
|
||||
// A sweep that read an object's mark before a send put it again must not remove it: the object, now without the mark, is left.
|
||||
func TestASweepDoesNotRemoveAnObjectASendPutAgainAfterItWasJudged(t *testing.T) { // 1696/AC8
|
||||
n := strings.Repeat("4", 64)
|
||||
t0 := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)
|
||||
b := &sweepBucketFake{objects: map[string]time.Time{n: t0}}
|
||||
judged := bucketObject{Name: n, UnnamedSince: t0}
|
||||
b.objects[n] = time.Time{} // the send put it again: no mark
|
||||
if gone, err := b.Delete(t.Context(), judged.Name, judged.UnnamedSince); err != nil || gone || len(b.deleted) != 0 {
|
||||
t.Fatalf("an object put again was removed: %v %v %v", gone, err, b.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// One node's bucket is held by one act at a time in this process: the sweep waits for a send that has put its objects and not yet
|
||||
// published the declaration that names them (1696/AC9).
|
||||
func TestTheSweepOfANodesBucketWaitsForASendThatHasPutItsObjects(t *testing.T) { // 1696/AC9
|
||||
unlock := lockFilesOf("lock-test-node")
|
||||
got := make(chan struct{})
|
||||
go func() {
|
||||
defer lockFilesOf("lock-test-node")()
|
||||
close(got)
|
||||
}()
|
||||
select {
|
||||
case <-got:
|
||||
t.Fatal("the sweep took the node's bucket while a send held it")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
other := lockFilesOf("lock-test-other-node") // another node's bucket is not held
|
||||
other()
|
||||
unlock()
|
||||
select {
|
||||
case <-got:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("the sweep did not take the node's bucket once the send was done")
|
||||
}
|
||||
}
|
||||
|
||||
// A put that fails send after send raises a condition; the first failure does not (the grant it waits for travels in that very
|
||||
// declaration), and a put that works clears it (1696/AC10).
|
||||
func TestAPutThatFailsSendAfterSendRaisesAConditionAndClearsWhenItWorks(t *testing.T) { // 1696/AC10
|
||||
store := conditions.NewInMemory()
|
||||
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store,
|
||||
Teller: &conditions.Told{}, Now: func() time.Time { return time.Date(2026, 10, 21, 14, 0, 0, 0, time.UTC) }})
|
||||
t.Cleanup(func() { k.Close(t.Context()) })
|
||||
was := conditionsFrom
|
||||
conditionsFrom = k
|
||||
t.Cleanup(func() { conditionsFrom = was })
|
||||
key := filesNotPutObservation("cond-node", "").Key()
|
||||
open := func() bool {
|
||||
_, found, err := conditions.ReadOne(t.Context(), store, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return found
|
||||
}
|
||||
boom := errors.New("permissions violation")
|
||||
notePut(t.Context(), "cond-node", boom)
|
||||
if open() {
|
||||
t.Fatal("the first failure raised the condition")
|
||||
}
|
||||
notePut(t.Context(), "cond-node", boom)
|
||||
if !open() {
|
||||
t.Fatal("the second failure in a row did not raise the condition")
|
||||
}
|
||||
notePut(t.Context(), "cond-node", nil)
|
||||
if open() {
|
||||
t.Fatal("a put that worked did not clear the condition")
|
||||
}
|
||||
notePut(t.Context(), "cond-node", boom)
|
||||
if open() {
|
||||
t.Fatal("the count did not start again after a put that worked")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user