platform/catalog: generic ProjectionStore[T] consumer + race-free holder; finish C1
- Generic SnapshotWriter[T]/SnapshotReader[T] over a value-agnostic raw layer; SnapshotHolder[T] now RWMutex-guards munmap vs in-flight Get (race-free swap, TestSnapshotHolder_SwapRace) — the drain grace is now just close-batching. - ProjectionStore[T]: snapshot begin|chunk|end assembly → atomic swap, epoch-gated delta overlay, tombstones, atomic epoch sidecar (ordered before the swap), cold-start reopen, mandatory ProjectionTransform[T]. Future-epoch deltas are dropped by contract (recover via next snapshot), not buffered. - Shared bus-framing constants so producer + consumer agree. - Bump slask-finder / go-cart-actor / backoffice pointers; refresh status.md + catalog-projection.md to the finished state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5a3ffcb6bc
commit
8b454bccac
8 files changed
+959
-170
No files matched your search
+43
-43
@@ -20,12 +20,12 @@ func TestSnapshotRoundTrip(t *testing.T) {
|
||||
path := filepath.Join(dir, "snap.bin")
|
||||
|
||||
const n = 1000
|
||||
w := NewSnapshotWriter().WithTenant("default")
|
||||
w := NewSnapshotWriter[Projection]().WithTenant("default")
|
||||
// intentionally shuffled input — sort is what reorders at Write.
|
||||
perm := rand.Perm(n)
|
||||
for _, i := range perm {
|
||||
sku := fmt.Sprintf("SKU-%05d", i)
|
||||
if err := w.Append(makeProjection(sku, int64(100+(i%97)*100))); err != nil {
|
||||
if err := appendP(w, makeProjection(sku, int64(100+(i%97)*100))); err != nil {
|
||||
t.Fatalf("Append %s: %v", sku, err)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func TestSnapshotRoundTrip(t *testing.T) {
|
||||
t.Errorf("Write returned %q expected %q", final, path)
|
||||
}
|
||||
|
||||
r, err := ReadSnapshotFile(path)
|
||||
r, err := ReadSnapshotFile[Projection](path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadSnapshotFile: %v", err)
|
||||
}
|
||||
@@ -49,8 +49,8 @@ func TestSnapshotRoundTrip(t *testing.T) {
|
||||
if r.Len() != n {
|
||||
t.Errorf("Len: got %d want %d", r.Len(), n)
|
||||
}
|
||||
if r.Tenant != "default" {
|
||||
t.Errorf("Tenant: got %q want %q", r.Tenant, "default")
|
||||
if r.Tenant() != "default" {
|
||||
t.Errorf("Tenant: got %q want %q", r.Tenant(), "default")
|
||||
}
|
||||
|
||||
// every SKU present and correct
|
||||
@@ -79,19 +79,19 @@ func TestSnapshotRoundTrip(t *testing.T) {
|
||||
// — this test pins that invariant.
|
||||
func TestSnapshotCompareSKUInvariant(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
w := NewSnapshotWriter()
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
// deliberately inverted order after Append (so sort has work to do)
|
||||
perm := rand.Perm(500)
|
||||
for _, i := range perm {
|
||||
sku := fmt.Sprintf("XX-%05d", i)
|
||||
if err := w.Append(makeProjection(sku, int64(i))); err != nil {
|
||||
if err := appendP(w, makeProjection(sku, int64(i))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := w.Write(dir, "snap.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := ReadSnapshotFile(filepath.Join(dir, "snap.bin"))
|
||||
r, err := ReadSnapshotFile[Projection](filepath.Join(dir, "snap.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,8 +101,8 @@ func TestSnapshotCompareSKUInvariant(t *testing.T) {
|
||||
// form, and assert non-decreasing order under compareSKU.
|
||||
prevSKU := ""
|
||||
for i := 0; i < r.Len(); i++ {
|
||||
recOff := int(r.recordStart) + i*recordSize
|
||||
raw := r.data[recOff : recOff+32]
|
||||
recOff := int(r.raw.recordStart) + i*recordSize
|
||||
raw := r.raw.data[recOff : recOff+32]
|
||||
// trim trailing zeros
|
||||
end := len(raw)
|
||||
for end > 0 && raw[end-1] == 0 {
|
||||
@@ -131,45 +131,45 @@ func runesToPadded32(s string) []byte {
|
||||
// with no on-disk evidence.
|
||||
func TestSnapshotTenantRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
w := NewSnapshotWriter().WithTenant("bygglagret")
|
||||
if err := w.Append(makeProjection("ABC-1", 100)); err != nil {
|
||||
w := NewSnapshotWriter[Projection]().WithTenant("bygglagret")
|
||||
if err := appendP(w, makeProjection("ABC-1", 100)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(dir, "snap.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := ReadSnapshotFile(filepath.Join(dir, "snap.bin"))
|
||||
r, err := ReadSnapshotFile[Projection](filepath.Join(dir, "snap.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
if r.Tenant != "bygglagret" {
|
||||
t.Errorf("Tenant: got %q want %q", r.Tenant, "bygglagret")
|
||||
if r.Tenant() != "bygglagret" {
|
||||
t.Errorf("Tenant: got %q want %q", r.Tenant(), "bygglagret")
|
||||
}
|
||||
|
||||
// Empty tenant writes zero-padded.
|
||||
w2 := NewSnapshotWriter()
|
||||
if err := w2.Append(makeProjection("ABC-2", 200)); err != nil {
|
||||
w2 := NewSnapshotWriter[Projection]()
|
||||
if err := appendP(w2, makeProjection("ABC-2", 200)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w2.Write(dir, "snap2.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r2, err := ReadSnapshotFile(filepath.Join(dir, "snap2.bin"))
|
||||
r2, err := ReadSnapshotFile[Projection](filepath.Join(dir, "snap2.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r2.Close()
|
||||
if r2.Tenant != "" {
|
||||
t.Errorf("Tenant (no tag): got %q want empty", r2.Tenant)
|
||||
if r2.Tenant() != "" {
|
||||
t.Errorf("Tenant (no tag): got %q want empty", r2.Tenant())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotTenantTooLong — over-long tenant name rejected at Write.
|
||||
func TestSnapshotTenantTooLong(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
w := NewSnapshotWriter().WithTenant(string(make([]byte, 33)))
|
||||
if err := w.Append(makeProjection("ABC", 1)); err != nil {
|
||||
w := NewSnapshotWriter[Projection]().WithTenant(string(make([]byte, 33)))
|
||||
if err := appendP(w, makeProjection("ABC", 1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(dir, "snap.bin"); err == nil {
|
||||
@@ -196,7 +196,7 @@ func TestSnapshotCountOverflowRejected(t *testing.T) {
|
||||
if err := os2WriteFile(path, bs, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotOverflow) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotOverflow) {
|
||||
t.Errorf("overflow count rejected with %v, want ErrSnapshotOverflow", err)
|
||||
}
|
||||
}
|
||||
@@ -205,10 +205,10 @@ func TestSnapshotCountOverflowRejected(t *testing.T) {
|
||||
// leaves a half-written snap.bin visible to readers.
|
||||
func TestSnapshotAtomicSwap(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
w := NewSnapshotWriter()
|
||||
mustAppend := func(w *SnapshotWriter, sku string, price int64) {
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
mustAppend := func(w *SnapshotWriter[Projection], sku string, price int64) {
|
||||
t.Helper()
|
||||
if err := w.Append(makeProjection(sku, price)); err != nil {
|
||||
if err := appendP(w, makeProjection(sku, price)); err != nil {
|
||||
t.Fatalf("Append %s: %v", sku, err)
|
||||
}
|
||||
}
|
||||
@@ -223,13 +223,13 @@ func TestSnapshotAtomicSwap(t *testing.T) {
|
||||
}
|
||||
|
||||
// replace
|
||||
w2 := NewSnapshotWriter()
|
||||
w2 := NewSnapshotWriter[Projection]()
|
||||
mustAppend(w2, "ZZZ", 200)
|
||||
if _, err := w2.Write(dir, "snap.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := ReadSnapshotFile(final)
|
||||
r, err := ReadSnapshotFile[Projection](final)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -251,7 +251,7 @@ func TestSnapshotRejectBadInput(t *testing.T) {
|
||||
if err := os2WriteFile(path, []byte{}, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotTruncated) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotTruncated) {
|
||||
t.Errorf("empty file rejected with %v, want ErrSnapshotTruncated", err)
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ func TestSnapshotRejectBadInput(t *testing.T) {
|
||||
if err := os2WriteFile(path, wrongMagic, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotMagic) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotMagic) {
|
||||
t.Errorf("wrong magic rejected with %v, want ErrSnapshotMagic", err)
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func TestSnapshotRejectBadInput(t *testing.T) {
|
||||
if err := os2WriteFile(path, buildValidHeader(5), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotTruncated) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotTruncated) {
|
||||
t.Errorf("truncated snapshot rejected with %v, want ErrSnapshotTruncated", err)
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func TestSnapshotRejectBadInput(t *testing.T) {
|
||||
if err := os2WriteFile(path, tampered, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotBadBlob) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotBadBlob) {
|
||||
t.Errorf("tampered-blob rejected with %v, want ErrSnapshotBadBlob", err)
|
||||
}
|
||||
}
|
||||
@@ -292,11 +292,11 @@ func TestSnapshotRejectBadInput(t *testing.T) {
|
||||
// TestSnapshotEmpty — empty (count=0) snapshot is valid; Get returns false.
|
||||
func TestSnapshotEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
w := NewSnapshotWriter()
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
if _, err := w.Write(dir, "empty.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := ReadSnapshotFile(filepath.Join(dir, "empty.bin"))
|
||||
r, err := ReadSnapshotFile[Projection](filepath.Join(dir, "empty.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -316,14 +316,14 @@ func TestSnapshotSKU32(t *testing.T) {
|
||||
if len(sku) != 32 {
|
||||
t.Fatalf("test bug: sku length %d", len(sku))
|
||||
}
|
||||
w := NewSnapshotWriter()
|
||||
if err := w.Append(makeProjection(sku, 1)); err != nil {
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
if err := appendP(w, makeProjection(sku, 1)); err != nil {
|
||||
t.Fatalf("Append 32-char SKU: %v", err)
|
||||
}
|
||||
if _, err := w.Write(dir, "snap.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := ReadSnapshotFile(filepath.Join(dir, "snap.bin"))
|
||||
r, err := ReadSnapshotFile[Projection](filepath.Join(dir, "snap.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -335,9 +335,9 @@ func TestSnapshotSKU32(t *testing.T) {
|
||||
|
||||
// TestSnapshotSKU33Rejected — one char over should error at Append.
|
||||
func TestSnapshotSKU33Rejected(t *testing.T) {
|
||||
w := NewSnapshotWriter()
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
sku := repeat32("X", 33)
|
||||
err := w.Append(makeProjection(sku, 1))
|
||||
err := appendP(w, makeProjection(sku, 1))
|
||||
if !errors.Is(err, ErrSKUTooLong) {
|
||||
t.Errorf("33-char SKU Append: got %v want ErrSKUTooLong", err)
|
||||
}
|
||||
@@ -351,18 +351,18 @@ func TestSnapshotGetSpeed(t *testing.T) {
|
||||
}
|
||||
dir := t.TempDir()
|
||||
const n = 5000
|
||||
w := NewSnapshotWriter()
|
||||
w := NewSnapshotWriter[Projection]()
|
||||
skus := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
skus[i] = fmt.Sprintf("Z%06d", i)
|
||||
if err := w.Append(makeProjection(skus[i], int64(i))); err != nil {
|
||||
if err := appendP(w, makeProjection(skus[i], int64(i))); err != nil {
|
||||
t.Fatalf("Append %s: %v", skus[i], err)
|
||||
}
|
||||
}
|
||||
if _, err := w.Write(dir, "snap.bin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := ReadSnapshotFile(filepath.Join(dir, "snap.bin"))
|
||||
r, err := ReadSnapshotFile[Projection](filepath.Join(dir, "snap.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -429,7 +429,7 @@ func TestSnapshotRejectBadVersion(t *testing.T) {
|
||||
if err := os2WriteFile(path, bs, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadSnapshotFile(path); !errors.Is(err, ErrSnapshotVersion) {
|
||||
if _, err := ReadSnapshotFile[Projection](path); !errors.Is(err, ErrSnapshotVersion) {
|
||||
t.Errorf("v2-not-supported header rejected with %v, want ErrSnapshotVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user