Harden chunk plan cache storage and reuse

This commit is contained in:
2026-07-18 00:49:50 +00:00
parent 6fc6ce0adb
commit 6d0a19c94c
7 changed files with 228 additions and 5 deletions

View File

@@ -3,6 +3,8 @@ package chunkplan
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
@@ -125,6 +127,7 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
{name: "unknown field", mutate: func(data []byte) []byte {
return bytes.Replace(data, []byte(`{"schema_version"`), []byte(`{"unknown":true,"schema_version"`), 1)
}, want: "unknown"},
{name: "truncated JSON", mutate: func(data []byte) []byte { return data[:len(data)/2] }, want: "decode"},
{name: "schema mismatch", mutate: replaceJSON(`notarius.chunk-plan.v1`, `notarius.chunk-plan.v2`), want: "schema_version"},
{name: "source mismatch", mutate: replaceJSON(testSourceDigest, "sha256:"+strings.Repeat("b", 64)), want: "source_digest"},
{name: "plan digest mismatch", mutate: func(data []byte) []byte {
@@ -138,6 +141,9 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
{name: "noncanonical annotation", mutate: func(data []byte) []byte {
return bytes.Replace(data, []byte(`{"value":1}`), []byte(`{ "value": 1 }`), 1)
}, want: "canonical JSON"},
{name: "bad boundary", mutate: func(data []byte) []byte {
return bytes.Replace(data, []byte(`"start_unit_id":1`), []byte(`"start_unit_id":0`), 1)
}, want: "boundaries must be positive"},
{name: "trailing JSON", mutate: func(data []byte) []byte { return append(data, []byte(` {}`)...) }, want: "trailing"},
}
for _, tc := range tests {
@@ -226,6 +232,77 @@ func TestFilesystemStoreConcurrentWritersExposeCompleteRecord(t *testing.T) {
}
}
func TestFilesystemStoreReadersObserveOnlyCompleteRecordsDuringWrites(t *testing.T) {
store := newStore(t, t.TempDir())
if err := store.Save(testRecord(t, 1)); err != nil {
t.Fatal(err)
}
const writers = 12
const readers = 12
errs := make(chan error, writers+readers)
start := make(chan struct{})
var writersDone sync.WaitGroup
for i := 0; i < writers; i++ {
writersDone.Add(1)
go func(value int) {
defer writersDone.Done()
<-start
errs <- store.Save(testRecord(t, value+2))
}(i)
}
for i := 0; i < readers; i++ {
go func() {
<-start
for attempt := 0; attempt < 50; attempt++ {
record, decision, err := store.Load(testSourceDigest)
if err != nil || decision.Status != pipeline.ChunkPlanHit {
errs <- fmt.Errorf("Load() decision=%#v error=%v", decision, err)
return
}
if err := validateRecord(record, testSourceDigest); err != nil {
errs <- fmt.Errorf("reader observed invalid record: %w", err)
return
}
}
errs <- nil
}()
}
close(start)
writersDone.Wait()
for i := 0; i < readers+writers; i++ {
if err := <-errs; err != nil {
t.Fatal(err)
}
}
}
func TestFilesystemStoreInterruptedWritesPreservePreviousRecord(t *testing.T) {
store := newStore(t, t.TempDir()).(*filesystemStore)
first := testRecord(t, 1)
if err := store.Save(first); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
hooks atomicWriteHooks
}{
{name: "before temporary file", hooks: atomicWriteHooks{BeforeCreateTemp: func() error { return errors.New("interrupted before temporary file") }}},
{name: "before rename", hooks: atomicWriteHooks{BeforeRename: func() error { return errors.New("interrupted before rename") }}},
} {
t.Run(tc.name, func(t *testing.T) {
store.write = func(target string, data []byte) error { return writeAtomicWithHooks(target, data, tc.hooks) }
if err := store.Save(testRecord(t, 2)); err == nil {
t.Fatal("Save() error = nil")
}
store.write = writeAtomic
got, decision, err := store.Load(testSourceDigest)
if err != nil || decision.Status != pipeline.ChunkPlanHit || !reflect.DeepEqual(got, first) {
t.Fatalf("record after interruption=%#v decision=%#v error=%v", got, decision, err)
}
})
}
}
func newStore(t *testing.T, root string) pipeline.ChunkPlanStore {
t.Helper()
store, err := NewFilesystemStore(root)