feat: enforce tiered storage reservations
This commit is contained in:
@@ -66,7 +66,7 @@ func TestRealSQLiteInitializationAndRestart_HP_STORE_01(t *testing.T) {
|
||||
if err := rows.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"audit_events", "clients", "command_events", "command_payloads", "command_tombstones", "commands", "control_mutations", "output_segments", "output_truncations", "schema_migrations", "sessions", "stdin_writes", "storage_incidents", "takeover_authorizations"}
|
||||
want := []string{"audit_events", "clients", "command_events", "command_payloads", "command_tombstones", "commands", "control_mutations", "output_segments", "output_truncations", "schema_migrations", "sessions", "stdin_writes", "storage_counters", "storage_incidents", "takeover_authorizations"}
|
||||
sort.Strings(want)
|
||||
if strings.Join(names, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("tables = %v, want %v", names, want)
|
||||
@@ -531,6 +531,112 @@ func TestEventGapRejectedBeforeFileWriteAndRotation_HP_STORE_03(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventQuotaCountersAtomicAndCapacityRejectsBeforeWrite_HP_STORE_07(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataDir := filepath.Join(t.TempDir(), "state")
|
||||
limits := store.QuotaLimits{
|
||||
HardAllocationBytes: 1000, CommandOutputBytes: 1000, CommandTotalBytes: 2000,
|
||||
ClientTotalBytes: 3000, ServerTotalBytes: 4000, CloseoutReserveBytes: 200, FilesystemFloorBytes: 50,
|
||||
}
|
||||
opened := openStoreWithOptions(t, store.Options{
|
||||
DataDir: dataDir, BusyTimeout: busyTimeout, QuotaLimits: limits,
|
||||
FreeSpaceProbe: store.FreeSpaceProbeFunc(func(string) (uint64, error) { return 10_000, nil }),
|
||||
})
|
||||
issue := uuidBytes(90)
|
||||
seedCommand(t, opened.DB(), issue)
|
||||
if _, err := opened.DB().Exec(`UPDATE commands SET closeout_remaining_bytes = ? WHERE issue_uuid = ?`, limits.CloseoutReserveBytes, issue[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := appendEvent(issue, 1, "first")
|
||||
if _, err := opened.AppendCommandEvent(context.Background(), first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var commandOutput, commandTotal, closeout, clientTotal, serverTotal uint64
|
||||
if err := opened.DB().QueryRow(`SELECT output_charged_bytes, charged_bytes, closeout_remaining_bytes FROM commands WHERE issue_uuid = ?`, issue[:]).Scan(&commandOutput, &commandTotal, &closeout); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := opened.DB().QueryRow(`SELECT charged_bytes FROM clients WHERE client_id = 'client-a'`).Scan(&clientTotal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := opened.DB().QueryRow(`SELECT command_charged_bytes FROM storage_counters WHERE singleton = 1`).Scan(&serverTotal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if commandOutput == 0 || commandOutput != commandTotal || commandTotal != clientTotal || clientTotal != serverTotal || closeout != limits.CloseoutReserveBytes {
|
||||
t.Fatalf("quota counters = output:%d command:%d client:%d server:%d closeout:%d", commandOutput, commandTotal, clientTotal, serverTotal, closeout)
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dataDir, "segments"))
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("segment entries = %v, err = %v", entries, err)
|
||||
}
|
||||
segmentPath := filepath.Join(dataDir, "segments", entries[0].Name())
|
||||
before, err := os.Stat(segmentPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result, err := opened.AppendCommandEvent(context.Background(), first); err != nil || !result.Duplicate {
|
||||
t.Fatalf("duplicate = (%+v, %v)", result, err)
|
||||
}
|
||||
second := appendEvent(issue, 2, "second")
|
||||
if _, err := opened.AppendCommandEvent(context.Background(), second); err == nil {
|
||||
t.Fatal("over-quota event was accepted")
|
||||
} else {
|
||||
var capacity *store.CapacityError
|
||||
if !errors.As(err, &capacity) || capacity.Tier != store.CapacityTierCommandOut {
|
||||
t.Fatalf("capacity error = %v", err)
|
||||
}
|
||||
}
|
||||
after, err := os.Stat(segmentPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Size() != before.Size() {
|
||||
t.Fatalf("rejected event changed segment size from %d to %d", before.Size(), after.Size())
|
||||
}
|
||||
assertCommandEventState(t, opened.DB(), issue, 1, 1)
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemFloorAndCounterMismatch_BH_STORE_08(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataDir := filepath.Join(t.TempDir(), "state")
|
||||
opened := openStoreWithOptions(t, store.Options{
|
||||
DataDir: dataDir, BusyTimeout: busyTimeout,
|
||||
FreeSpaceProbe: store.FreeSpaceProbeFunc(func(string) (uint64, error) { return 0, nil }),
|
||||
})
|
||||
issue := uuidBytes(110)
|
||||
seedCommand(t, opened.DB(), issue)
|
||||
if _, err := opened.AppendCommandEvent(context.Background(), appendEvent(issue, 1, "floor")); err == nil {
|
||||
t.Fatal("filesystem-floor event was accepted")
|
||||
} else {
|
||||
var capacity *store.CapacityError
|
||||
if !errors.As(err, &capacity) || capacity.Tier != store.CapacityTierFilesystem {
|
||||
t.Fatalf("floor error = %v", err)
|
||||
}
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dataDir, "segments"))
|
||||
if err != nil || len(entries) != 0 {
|
||||
t.Fatalf("segments after floor rejection = %v, err = %v", entries, err)
|
||||
}
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reopened := openStore(t, dataDir)
|
||||
if _, err := reopened.DB().Exec(`UPDATE clients SET charged_bytes = 1 WHERE client_id = 'client-a'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := reopened.RecoverCommandSegments(context.Background()); !errors.Is(err, store.ErrQuotaCounterMismatch) {
|
||||
t.Fatalf("counter mismatch recovery error = %v", 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})
|
||||
|
||||
Reference in New Issue
Block a user