fix: make WAL segment directory fsync failure fatal (C6)
Per design §3.2 line 248-272, segment directory fsync is a hard requirement for durable-ready state, not best-effort. rename is atomic in memory but not guaranteed to survive power loss without a directory fsync. The previous code silently swallowed both os.Open(dir) and dirFD.Sync() errors, leaving WAL writer to confirm batches as durable when their segment might not exist after a crash. Failure propagation: - Initial segment creation: NewSegmentWriter fails -> NewSegmentManager fails -> DB.Open fails (user sees error, no data promise violated). - Rotation during AppendBatch: NewSegmentWriter fails -> AppendBatch fails -> WalWriter.stopWithError(ErrCommitUnknown) -> write-stopped (per design line 272). Changes: - wal/segment_writer.go: extract dirFsync helper (Open -> f.Stat -> IsDir -> f.Sync, avoiding TOCTOU window), replace silent swallow with fatal error; on failure clean up resources (fd.Close + os.Remove) and surface cleanup errors via errors.Join so nothing is silently lost. - wal/dir_fsync_test.go (new): unit test the helper with valid dir, non-existent dir (fails at os.Open), and not-a-dir (fails at IsDir). - wal/segment_writer_test.go: add TestNewSegmentWriterDirFsyncFailure (injects failure via package-level dirFsyncFn override; documents the not-parallel-safe constraint), TestNewSegmentWriterNormalPathStillWorks (regression), and TestNewSegmentWriterRetryAfterDirFsyncFailure (verifies cleanup is effective for retry). - wal/segment_manager_test.go: add TestSegmentManagerRotateFailsOnDirFsyncFailure (fills segment until rotation triggers, injects failure, verifies propagation through AppendBatch path) and TestNewSegmentManagerFailsOnDirFsyncFailure (covers the DB.Open failure path). dirFsyncFn injection note: tests that override this package-level var must not use t.Parallel(). All existing wal tests run serially within the package; this is the lightest mechanism that doesn't require interface indirection in production code. Verified: each new test fails on pre-fix code (silent swallow returned nil error) and passes after the fix. Full suite green including go test -race ./... . Audit context: docs/audit-3.2.md C6 (Oracle-verified bg_ef425776).
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package wal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDirFsyncSuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := dirFsync(dir); err != nil {
|
||||
t.Errorf("dirFsync on valid dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirFsyncNonExistentDir(t *testing.T) {
|
||||
err := dirFsync("/nonexistent/path/that/should/not/exist")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on non-existent dir")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "open dir") {
|
||||
t.Errorf("error should mention 'open dir', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirFsyncNotADirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
filePath := dir + "/notadir"
|
||||
if err := os.WriteFile(filePath, []byte("x"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
err := dirFsync(filePath)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when fsyncing a file as dir")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not a directory") {
|
||||
t.Errorf("error should mention 'not a directory', got: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user