feat: reclaim terminal commands atomically
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
@@ -27,7 +28,7 @@ func TestRealSQLiteInitializationAndRestart_HP_STORE_01(t *testing.T) {
|
||||
dataDir := filepath.Join(t.TempDir(), "state")
|
||||
opened := openStore(t, dataDir)
|
||||
|
||||
for _, directory := range []string{dataDir, filepath.Join(dataDir, "segments"), filepath.Join(dataDir, "audit")} {
|
||||
for _, directory := range []string{dataDir, filepath.Join(dataDir, "segments"), filepath.Join(dataDir, "audit"), filepath.Join(dataDir, "deleting")} {
|
||||
info, err := os.Stat(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -637,6 +638,147 @@ func TestFilesystemFloorAndCounterMismatch_BH_STORE_08(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalAgeRetentionEvictsWholeCommand_HP_STORE_08(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataDir := filepath.Join(t.TempDir(), "state")
|
||||
opened := openStore(t, dataDir)
|
||||
now := time.Unix(2_000_000, 0).UTC()
|
||||
old := uuidBytes(120)
|
||||
active := uuidBytes(140)
|
||||
seedCommand(t, opened.DB(), old)
|
||||
seedCommand(t, opened.DB(), active)
|
||||
if _, err := opened.AppendCommandEvent(context.Background(), appendEvent(old, 1, "retained history")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
markTerminal(t, opened.DB(), old, now.Add(-40*24*time.Hour), now.Add(-31*24*time.Hour))
|
||||
if _, err := opened.DB().Exec(`UPDATE commands SET issue_time = ? WHERE issue_uuid = ?`, now.Add(-100*24*time.Hour).UnixNano(), active[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report, err := opened.RunRetention(context.Background(), now, store.RetentionPolicy{
|
||||
TerminalAge: 30 * 24 * time.Hour, TombstoneMaxEntries: 1_000_000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if report.CommandsEvicted != 1 || report.FilesDeleted != 1 || report.BytesReleased == 0 {
|
||||
t.Fatalf("retention report = %+v", report)
|
||||
}
|
||||
var commands, tombstones int
|
||||
if err := opened.DB().QueryRow(`SELECT count(*) FROM commands WHERE issue_uuid = ?`, old[:]).Scan(&commands); err != nil || commands != 0 {
|
||||
t.Fatalf("old command count = %d, err = %v", commands, err)
|
||||
}
|
||||
if err := opened.DB().QueryRow(`SELECT count(*) FROM commands WHERE issue_uuid = ?`, active[:]).Scan(&commands); err != nil || commands != 1 {
|
||||
t.Fatalf("active command count = %d, err = %v", commands, err)
|
||||
}
|
||||
if err := opened.DB().QueryRow(`SELECT count(*) FROM command_tombstones WHERE issue_uuid = ?`, old[:]).Scan(&tombstones); err != nil || tombstones != 1 {
|
||||
t.Fatalf("tombstone count = %d, err = %v", tombstones, err)
|
||||
}
|
||||
for _, directory := range []string{"segments", "deleting"} {
|
||||
entries, err := os.ReadDir(filepath.Join(dataDir, directory))
|
||||
if err != nil || len(entries) != 0 {
|
||||
t.Fatalf("%s entries = %v, err = %v", directory, entries, err)
|
||||
}
|
||||
}
|
||||
if _, err := opened.RecoverCommandSegments(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTombstoneFIFOIsCappedInEvictionTransaction_HP_STORE_09(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
opened := openStore(t, filepath.Join(t.TempDir(), "state"))
|
||||
now := time.Unix(3_000_000, 0).UTC()
|
||||
first := uuidBytes(150)
|
||||
second := uuidBytes(170)
|
||||
seedCommand(t, opened.DB(), first)
|
||||
seedCommand(t, opened.DB(), second)
|
||||
markTerminal(t, opened.DB(), first, now.Add(-2*time.Hour), now.Add(-time.Hour))
|
||||
markTerminal(t, opened.DB(), second, now.Add(-time.Hour), now.Add(-time.Hour))
|
||||
report, err := opened.RunRetention(context.Background(), now, store.RetentionPolicy{PressureBytes: 1, TombstoneMaxEntries: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Zero-charge commands cannot satisfy byte pressure, so every eligible terminal command is reclaimed.
|
||||
if report.CommandsEvicted != 2 || report.TombstonesTrimmed != 1 {
|
||||
t.Fatalf("retention report = %+v", report)
|
||||
}
|
||||
var count int
|
||||
var remaining []byte
|
||||
if err := opened.DB().QueryRow(`SELECT count(*), max(issue_uuid) FROM command_tombstones`).Scan(&count, &remaining); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 || !bytes.Equal(remaining, second[:]) {
|
||||
t.Fatalf("tombstones = count %d remaining %x, want %x", count, remaining, second)
|
||||
}
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetentionCrashStagesRollForward_CRASH_STORE_04(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for index, checkpoint := range []string{
|
||||
store.FaultAfterEvictionMarked,
|
||||
store.FaultAfterEvictionFilesMoved,
|
||||
store.FaultAfterEvictionMetadataGone,
|
||||
} {
|
||||
t.Run(checkpoint, func(t *testing.T) {
|
||||
dataDir := filepath.Join(t.TempDir(), "state")
|
||||
injected := errors.New("injected retention crash")
|
||||
opened := openStoreWithOptions(t, store.Options{
|
||||
DataDir: dataDir, BusyTimeout: busyTimeout,
|
||||
FaultInjector: &failOnceInjector{target: checkpoint, failure: injected},
|
||||
})
|
||||
now := time.Unix(4_000_000+int64(index), 0).UTC()
|
||||
issue := uuidBytes(byte(190 + index*20))
|
||||
seedCommand(t, opened.DB(), issue)
|
||||
if _, err := opened.AppendCommandEvent(context.Background(), appendEvent(issue, 1, "evict me")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
markTerminal(t, opened.DB(), issue, now.Add(-2*time.Hour), now.Add(-time.Hour))
|
||||
if _, err := opened.RunRetention(context.Background(), now, store.RetentionPolicy{PressureBytes: 1, TombstoneMaxEntries: 10}); !errors.Is(err, injected) {
|
||||
t.Fatalf("RunRetention error = %v", err)
|
||||
}
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reopened := openStore(t, dataDir)
|
||||
if _, err := reopened.RecoverEvictions(context.Background(), 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var commandCount, tombstoneCount int
|
||||
if err := reopened.DB().QueryRow(`SELECT count(*) FROM commands WHERE issue_uuid = ?`, issue[:]).Scan(&commandCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reopened.DB().QueryRow(`SELECT count(*) FROM command_tombstones WHERE issue_uuid = ?`, issue[:]).Scan(&tombstoneCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if commandCount != 0 || tombstoneCount != 1 {
|
||||
t.Fatalf("recovered counts = commands %d tombstones %d", commandCount, tombstoneCount)
|
||||
}
|
||||
for _, directory := range []string{"segments", "deleting"} {
|
||||
entries, err := os.ReadDir(filepath.Join(dataDir, directory))
|
||||
if err != nil || len(entries) != 0 {
|
||||
t.Fatalf("%s entries = %v, err = %v", directory, entries, err)
|
||||
}
|
||||
}
|
||||
if _, err := reopened.RecoverCommandSegments(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reopened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func openStore(t *testing.T, dataDir string) *store.Store {
|
||||
t.Helper()
|
||||
return openStoreWithOptions(t, store.Options{DataDir: dataDir, BusyTimeout: busyTimeout})
|
||||
@@ -746,3 +888,10 @@ func assertCommandEventState(t *testing.T, database *sql.DB, issue [16]byte, wan
|
||||
t.Fatalf("event state = (last=%d, rows=%d), want (%d, %d)", last, events, wantLast, wantEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func markTerminal(t *testing.T, database *sql.DB, issue [16]byte, issueTime, terminalTime time.Time) {
|
||||
t.Helper()
|
||||
if _, err := database.Exec(`UPDATE commands SET issue_time = ?, lifecycle = 5, terminal_time = ? WHERE issue_uuid = ?`, issueTime.UnixNano(), terminalTime.UnixNano(), issue[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user