Harden chunk plan cache storage and reuse
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user