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,517 @@
|
||||
# C6 修复方案:SegmentWriter 目录 fsync 失败必须硬错误
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **目标**:修复 `wal/segment_writer.go:86-89` 静默吞掉目录 fsync 错误的 durability 漏洞。让 `NewSegmentWriter` 在目录 fsync 失败时返回错误,传播到 `SegmentManager.AppendBatch` → 触发 `ErrCommitUnknown` + write-stopped(或 `DB.Open` 失败)。
|
||||
>
|
||||
> **交付**:
|
||||
> - 抽出 `dirFsync` helper 函数(独立可测)
|
||||
> - `NewSegmentWriter` 调用 helper,失败时清理资源并返回错误
|
||||
> - 用 package-level 变量支持测试注入失败
|
||||
> - 新增 3 个测试:helper 单元测试 × 2 + NewSegmentWriter 集成测试 × 1
|
||||
> - 单次 commit
|
||||
>
|
||||
> **预估工时**:1.25-1.75 小时
|
||||
> **风险**:低(单文件改动 + 测试,production 行为变化是"原本被吞的错误现在会冒出来",这正是设计意图)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Bug 摘要(audit C6)
|
||||
|
||||
`wal/segment_writer.go:85-89`:
|
||||
|
||||
```go
|
||||
// Sync directory to make rename durable (best-effort on Linux).
|
||||
if dirFD, derr := os.Open(dir); derr == nil {
|
||||
dirFD.Sync() // 错误被完全忽略
|
||||
dirFD.Close()
|
||||
}
|
||||
```
|
||||
|
||||
两层错误都被吞:
|
||||
1. `os.Open(dir)` 失败 → 整个 fsync 被跳过
|
||||
2. `dirFD.Sync()` 失败 → 错误丢失
|
||||
|
||||
注释写"best-effort on Linux"误导,但设计明确这是硬条件。
|
||||
|
||||
### 设计依据(§3.2 line 248-273)
|
||||
|
||||
新 segment durable-ready 协议第 5 步:
|
||||
|
||||
```
|
||||
1. create segment-N.wal.tmp
|
||||
2. write WAL File Header
|
||||
3. fsync segment-N.wal.tmp
|
||||
4. rename segment-N.wal.tmp → segment-N.wal
|
||||
5. fsync WAL directory ← 关键
|
||||
6. segment-N enters durable-ready state
|
||||
7. WAL writer may append batches whose recovery depends on segment-N
|
||||
```
|
||||
|
||||
line 272:
|
||||
|
||||
> 如果新 segment 进入 durable-ready 之前任一步失败,**该 segment 不得成为 active segment,也不得承载可确认写入**。如果此时尚未分配 sequence,可以重试创建或切换到其他 segment;如果 sequence 已分配或已有 WAL Batch 依赖该 segment,则按 WAL write failure 处理,**引擎进入 write-stopped 状态**。
|
||||
|
||||
### 为什么是 Always 模式最严重的 durability 漏洞
|
||||
|
||||
Linux rename 是内存中的原子操作,**不保证掉电后目录项可恢复**。要让 rename 落盘必须 fsync 目录本身。掉电场景下:
|
||||
|
||||
- rename 已执行但目录 fsync 没成功 → 重启后 segment 文件可能从目录里消失
|
||||
- 但 WAL writer 已经按 "Always 不丢已确认写入" 向调用方返回成功
|
||||
|
||||
这个 bug **平时不发作**(正常环境下 rename + 异步 write-back 都会成功),**只在掉电那一刻有概率中招**。是最阴险的类型。
|
||||
|
||||
### 错误传播路径(已正确,只需源头报错)
|
||||
|
||||
```
|
||||
NewSegmentWriter 失败(dir fsync error)
|
||||
├─ 在 NewSegmentManager 路径(DB.Open 首次创建 segment)
|
||||
│ → NewSegmentManager 失败 → DB.Open 失败
|
||||
│ → 用户看到 error,没数据承诺被违反 ✓
|
||||
└─ 在 SegmentManager.rotate 路径(segment 满了轮转)
|
||||
→ AppendBatch 失败 → WalWriter.stopWithError(ErrCommitUnknown)
|
||||
→ write-stopped ✓(符合设计 line 272)
|
||||
```
|
||||
|
||||
`segment_manager.go:35-37, 81-83` 已经透传 error,本次 fix 不需要改它。
|
||||
|
||||
---
|
||||
|
||||
## 执行计划
|
||||
|
||||
### Phase A:代码改动(30 分钟)
|
||||
|
||||
#### A.1 抽出 `dirFsync` helper
|
||||
|
||||
新文件 `wal/dir_fsync.go`(或加入现有 `segment_writer.go` 末尾):
|
||||
|
||||
> **Momus 修订(bg_90bcf359)**:helper 应该校验 path 是目录,否则传文件路径会"成功 fsync 了一个文件"但语义错误。加 `os.Stat` + `IsDir` 检查。
|
||||
>
|
||||
> **Oracle 修订(bg_48d53274)**:进一步用 `os.Open → f.Stat → IsDir → f.Sync` 顺序,消除 `os.Stat` + `os.Open` 之间的 TOCTOU 窗口。
|
||||
|
||||
```go
|
||||
package wal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// dirFsync opens the directory and fsyncs it. Required for durable-ready
|
||||
// state per design §3.2 line 258. This is a hard requirement, not
|
||||
// best-effort: rename is atomic in memory but not guaranteed to survive
|
||||
// power loss without a directory fsync.
|
||||
//
|
||||
// dirFsyncFn is a package-level variable so tests can inject failures
|
||||
// without an interface indirection in production code.
|
||||
//
|
||||
// NOT PARALLEL-SAFE: tests that override dirFsyncFn must not use t.Parallel().
|
||||
// All existing wal tests run serially within the package.
|
||||
var dirFsyncFn = dirFsync
|
||||
|
||||
func dirFsync(dir string) error {
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open dir %q: %w", dir, err)
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat dir %q: %w", dir, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path %q is not a directory", dir)
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return fmt.Errorf("fsync dir %q: %w", dir, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
> **关于 hook 触发的注释**:上面 docstring 是必要的 —— 它解释了**为什么这是硬条件而不是 best-effort**(防止未来工程师把它"简化"成 silent again)。属于 security-related 注释,符合 hook 规则的 necessary comments。
|
||||
|
||||
#### A.2 修改 `NewSegmentWriter`
|
||||
|
||||
替换 `wal/segment_writer.go:85-89`:
|
||||
|
||||
```go
|
||||
// 改前(C6 bug):
|
||||
// Sync directory to make rename durable (best-effort on Linux).
|
||||
if dirFD, derr := os.Open(dir); derr == nil {
|
||||
dirFD.Sync()
|
||||
dirFD.Close()
|
||||
}
|
||||
|
||||
// 改后:
|
||||
// Per design §3.2 line 258, directory fsync is a hard requirement for
|
||||
// durable-ready. Without it, the rename above is not guaranteed to survive
|
||||
// power loss, violating the Always-mode "no loss of acknowledged writes"
|
||||
// promise.
|
||||
if err := dirFsyncFn(dir); err != nil {
|
||||
closeErr := fd.Close()
|
||||
removeErr := os.Remove(finalPath)
|
||||
if closeErr != nil || removeErr != nil {
|
||||
// Surface cleanup errors alongside the primary fsync error so they
|
||||
// are not lost (Go 1.20+ errors.Join).
|
||||
cleanup := errors.Join(closeErr, removeErr)
|
||||
return nil, fmt.Errorf("wal: fsync directory after segment rename (cleanup: %v): %w", cleanup, err)
|
||||
}
|
||||
return nil, fmt.Errorf("wal: fsync directory after segment rename: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
清理规则:失败时关闭新打开的 `fd`,删除已 rename 出来的 `finalPath`,让磁盘状态回到"没有 segment-N"。如果清理本身也失败,用 `errors.Join` 把清理错误和主错误一起返回(不丢任何信息)。
|
||||
|
||||
> **关于 hook 触发的注释**:这条注释引用设计行号 + 解释 failure 处理逻辑,防止未来"简化"成 silent。属于 security-related 注释。
|
||||
|
||||
### Phase B:测试(45-60 分钟)
|
||||
|
||||
#### B.1 helper 单元测试
|
||||
|
||||
新增 `wal/dir_fsync_test.go`:
|
||||
|
||||
```go
|
||||
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")
|
||||
}
|
||||
// After Oracle修订 (Open first), non-existent dir fails at os.Open.
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B.2 NewSegmentWriter 集成测试(注入失败)
|
||||
|
||||
新增到 `wal/segment_writer_test.go`:
|
||||
|
||||
```go
|
||||
// Regression guard for C6: NewSegmentWriter must return an error when
|
||||
// directory fsync fails, instead of silently succeeding. Per design §3.2
|
||||
// line 272, segment must NOT become active if durable-ready fails.
|
||||
func TestNewSegmentWriterDirFsyncFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Defaults()
|
||||
|
||||
// Inject dir fsync failure.
|
||||
orig := dirFsyncFn
|
||||
dirFsyncFn = func(d string) error {
|
||||
return errors.New("simulated dir fsync failure")
|
||||
}
|
||||
t.Cleanup(func() { dirFsyncFn = orig })
|
||||
|
||||
sw, err := NewSegmentWriter(dir, 0, 0, &cfg)
|
||||
if err == nil {
|
||||
if sw != nil {
|
||||
sw.Close()
|
||||
}
|
||||
t.Fatal("NewSegmentWriter: expected error on dir fsync failure, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "fsync directory") {
|
||||
t.Errorf("error should mention 'fsync directory', got: %v", err)
|
||||
}
|
||||
|
||||
// Verify cleanup: no segment file should remain.
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir: %v", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.Contains(name, "segment-0") {
|
||||
t.Errorf("segment file should be cleaned up, found: %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regression guard for C6: ensure normal path still works after the fix.
|
||||
func TestNewSegmentWriterNormalPathStillWorks(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Defaults()
|
||||
|
||||
sw, err := NewSegmentWriter(dir, 0, 0, &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSegmentWriter normal path: %v", err)
|
||||
}
|
||||
defer sw.Close()
|
||||
|
||||
// Verify segment file exists.
|
||||
if _, err := os.Stat(sw.SegmentPath()); err != nil {
|
||||
t.Errorf("segment file should exist: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B.3 验证错误传播
|
||||
|
||||
`wal/segment_manager_test.go` 已经测过 NewSegmentManager 正常路径。需补充测试验证错误传播。
|
||||
|
||||
> **Oracle 修订(bg_48d53274)**:原计划的 `_, err = sm.AppendBatch(encoded)` 是编译错误(`AppendBatch` 只返回 `error`)。另外,单个 `{k,v}` batch 编码只有 ~24 字节,远不到 512-32=480 阈值,**不会触发轮转**。需要循环填到 `sm.RemainingPayload() < worstCaseSize` 再注入失败。
|
||||
|
||||
```go
|
||||
// Regression guard for C6: SegmentManager.AppendBatch must propagate
|
||||
// rotation failure (which now includes dir fsync failure) as error.
|
||||
func TestSegmentManagerRotateFailsOnDirFsyncFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := tinyWalConfig()
|
||||
|
||||
sm, err := NewSegmentManager(dir, 0, 0, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSegmentManager: %v", err)
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
encoded, err := EncodeWalBatch(0, []*WalEntry{
|
||||
{OpType: OpPut, ValueKind: VKInline, Key: []byte("k"), Value: []byte("v")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeWalBatch: %v", err)
|
||||
}
|
||||
|
||||
// Fill the active segment until next AppendBatch would trigger rotation.
|
||||
// segment_manager.go:62 triggers rotate when
|
||||
// RemainingPayload() < len(encoded) + 2*PhysicalRecordHeaderSize
|
||||
worstCaseSize := uint64(len(encoded)) + 2*uint64(PhysicalRecordHeaderSize)
|
||||
for sm.RemainingPayload() >= worstCaseSize {
|
||||
if err := sm.AppendBatch(encoded); err != nil {
|
||||
t.Fatalf("fill AppendBatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject dir fsync failure for the next segment creation.
|
||||
orig := dirFsyncFn
|
||||
dirFsyncFn = func(string) error { return errors.New("simulated dir fsync failure") }
|
||||
t.Cleanup(func() { dirFsyncFn = orig })
|
||||
|
||||
// Next AppendBatch must trigger rotation and fail with propagated error.
|
||||
if err := sm.AppendBatch(encoded); err == nil {
|
||||
t.Fatal("AppendBatch: expected rotation failure, got nil")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:C8 的存在让"填满 segment 触发轮转"的实际行为不可预测(segment_manager.go:64-66 把字节偏移当 startSequence)。这个测试主要验证 dir fsync 失败的传播路径,不验证轮转后的 recovery。如果测试在 C8 修复前运行不稳定,可放宽为"只需证明错误被传播",不验证后续 recovery。
|
||||
|
||||
#### B.4 验证 NewSegmentManager 路径 + 清理(Oracle 新增)
|
||||
|
||||
> **Oracle 修订(bg_48d53274)**:原计划只覆盖了 NewSegmentWriter 单元层。需要单独覆盖 NewSegmentManager 包装路径(segment_manager.go:35-37),以及验证 dir fsync 失败后 retry 能成功(清理干净)。
|
||||
|
||||
```go
|
||||
// Regression guard for C6: NewSegmentManager must propagate dir fsync
|
||||
// failure from initial segment creation. This is the DB.Open failure path.
|
||||
func TestNewSegmentManagerFailsOnDirFsyncFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := tinyWalConfig()
|
||||
|
||||
orig := dirFsyncFn
|
||||
dirFsyncFn = func(string) error { return errors.New("simulated dir fsync failure") }
|
||||
t.Cleanup(func() { dirFsyncFn = orig })
|
||||
|
||||
sm, err := NewSegmentManager(dir, 0, 0, cfg)
|
||||
if err == nil {
|
||||
if sm != nil {
|
||||
sm.Close()
|
||||
}
|
||||
t.Fatal("NewSegmentManager: expected error on dir fsync failure, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "create initial segment") {
|
||||
t.Errorf("error should be wrapped as 'create initial segment', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Regression guard for C6: after a failed NewSegmentWriter due to dir
|
||||
// fsync, retrying with fsync restored must succeed and not leak state
|
||||
// (no leftover segment files).
|
||||
func TestNewSegmentWriterRetryAfterDirFsyncFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Defaults()
|
||||
|
||||
// First attempt: inject failure.
|
||||
orig := dirFsyncFn
|
||||
dirFsyncFn = func(string) error { return errors.New("simulated") }
|
||||
_, err := NewSegmentWriter(dir, 0, 0, &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected first NewSegmentWriter to fail")
|
||||
}
|
||||
dirFsyncFn = orig
|
||||
|
||||
// Second attempt: must succeed (cleanup was effective).
|
||||
sw, err := NewSegmentWriter(dir, 0, 0, &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("retry NewSegmentWriter: %v", err)
|
||||
}
|
||||
defer sw.Close()
|
||||
|
||||
// Verify no leftover .tmp files from the failed attempt.
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir: %v", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".tmp") {
|
||||
t.Errorf("leftover .tmp file: %s", e.Name())
|
||||
}
|
||||
// segment-0.wal should exist (from successful retry).
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase C:验证(15 分钟)
|
||||
|
||||
```bash
|
||||
# 1. 编译通过
|
||||
go build ./...
|
||||
|
||||
# 2. 重点测试(C6 新增的全部)
|
||||
go test ./wal -run 'TestDirFsync|TestNewSegmentWriter|TestSegmentManagerRotateFailsOnDirFsyncFailure|TestNewSegmentManagerFailsOnDirFsyncFailure' -count=1 -v
|
||||
|
||||
# 3. wal 包全量
|
||||
go test ./wal/... -count=1
|
||||
|
||||
# 4. 全仓
|
||||
go test ./... -count=1
|
||||
|
||||
# 5. race 全仓(Oracle 修订:不只 wal + root,跑 ./...)
|
||||
go test -race ./... -count=1
|
||||
|
||||
# 6. vet
|
||||
go vet ./...
|
||||
|
||||
# 7. lint(如果装了)
|
||||
if command -v golangci-lint >/dev/null 2>&1; then
|
||||
golangci-lint run ./wal/...
|
||||
else
|
||||
echo "golangci-lint not installed, skipping"
|
||||
fi
|
||||
```
|
||||
|
||||
### Phase D:Commit message draft
|
||||
|
||||
```
|
||||
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).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] Phase A.1:`dirFsync` helper 存在,docstring 引用设计行号,注释 "NOT PARALLEL-SAFE"
|
||||
- [ ] Phase A.2:`NewSegmentWriter` 调用 `dirFsyncFn`,失败时 `errors.Join` 合并 fd.Close + os.Remove 错误
|
||||
- [ ] Phase B.1:3 个 helper 单元测试存在,错误期望正确("open dir" / "not a directory")
|
||||
- [ ] Phase B.2:2 个 NewSegmentWriter 测试(failure + regression)存在
|
||||
- [ ] Phase B.3:rotate 失败测试存在,编译正确,用循环填到 `RemainingPayload() < worstCaseSize`
|
||||
- [ ] Phase B.4:NewSegmentManager 失败测试 + retry-after-failure 测试存在
|
||||
- [ ] `go test ./wal/... -count=1` 全绿
|
||||
- [ ] `go test ./... -count=1` 全绿
|
||||
- [ ] `go test -race ./... -count=1` 全绿
|
||||
- [ ] `go vet ./...` 无新增警告
|
||||
- [ ] 单次 commit,message 引用 audit C6
|
||||
|
||||
---
|
||||
|
||||
## 不在本次范围内(后续 issue)
|
||||
|
||||
| 编号 | 为什么不放进来 |
|
||||
|------|---------------|
|
||||
| **C8** | segment_manager 把字节偏移当 startSequence,多 segment recovery 直接坏。和 C6 完全独立,但优先级也很高,可单独做 |
|
||||
| C4 | 涉及 `RecoverFromSegments` 接口变更(isLast 参数),改动面更大 |
|
||||
| C5 + H8 | 都在 truncation 路径,应一起做(fsync + 删空 segment + dir fsync + findValidOffset batch 边界) |
|
||||
| C1 | CRC 多项式错,跨多个文件,需要测试向量 |
|
||||
| C7 | Put/Close 并发竞态,独立 |
|
||||
| H1-H7 | 其他 High 级别问题 |
|
||||
|
||||
---
|
||||
|
||||
## 修订记录
|
||||
|
||||
- **v1(原始)**:C6 修复方案初稿,送 Momus 审
|
||||
- **v1.1(Momus 修订 bg_90bcf359)**:
|
||||
- helper 加 `os.Stat` + `IsDir` 校验,避免传文件路径误"成功"
|
||||
- rotate 测试改用现成的 `tinyWalConfig()` helper(同时减 BlockSize + MaxBatchSize + MaxSegmentSize),避免单独设小 segment 触发 config.Validate 失败
|
||||
- **v1.2(Oracle 修订 bg_48d53274)**:
|
||||
- **BLOCKING**:helper 改为 `os.Open → f.Stat → IsDir → f.Sync` 顺序,消除 TOCTOU 窗口;同步修正 `TestDirFsyncNonExistentDir` 错误期望("open dir" 而非 "stat dir")
|
||||
- **BLOCKING**:`TestDirFsyncNotADirectory` 缺 `os` import,补齐
|
||||
- **BLOCKING**:`TestSegmentManagerRotateFailsOnDirFsyncFailure` 编译错(`_, err =` 应为 `err =`);单个 batch 不会触发轮转,改为循环填到 `RemainingPayload() < worstCaseSize`
|
||||
- **NEW**:加 `TestNewSegmentManagerFailsOnDirFsyncFailure`(覆盖 NewSegmentManager→DB.Open 失败路径)
|
||||
- **NEW**:加 `TestNewSegmentWriterRetryAfterDirFsyncFailure`(验证清理后 retry 能成功)
|
||||
- **MINOR**:A.2 cleanup 用 `errors.Join` 把 close/remove 错误和主错误合并,不丢任何信息
|
||||
- **MINOR**:`dirFsyncFn` 旁注释 "NOT PARALLEL-SAFE"
|
||||
- **MINOR**:race 改成 `go test -race ./...`(全仓,不只 wal + root)
|
||||
Reference in New Issue
Block a user