fix: treat non-last segment corruption as hard error (C4)

Per design §3.2 line 704, tail corruption in a non-last WAL segment is
middle corruption, which must hard-fail recovery instead of being
silently truncated. The previous code in wal/recovery.go always returned
TailCorruptionError for CollectingFragments state or parse errors,
regardless of segment position. Recover then always truncated
segments[last], which could corrupt a valid last segment when the actual
corruption was in a middle segment.

Oracle bg_ef425776 flagged an additional failure mode: "wal/recover.go
总是对 segments[len(segments)-1] 调用截断,但 RecoverFromSegments 的
TailCorruptionError 可能来自非尾段".

Changes:
- wal/record_parser.go: add SegmentPath field to TailCorruptionError for
  diagnostics and defensive truncation target identification.
- wal/recovery.go:
  - ReplaySegmentFile now takes isLastSegment bool parameter.
  - When parse error or CollectingFragments occurs in non-last segment,
    return hard error. Uses %v (NOT %w) so IsTailCorruption returns false
    — otherwise errors.As would still find underlying TailCorruptionError
    through the %w chain and Recover would treat it as truncatable.
  - When in last segment, return TailCorruptionError with SegmentPath set.
  - RecoverFromSegments passes isLastSegment based on iteration index.
- wal/recover.go:
  - Use tce.SegmentPath as authoritative truncation target (defensive
    fallback to segments[last] if missing). After C4 fix, TailCorruptionError
    is only returned for last segment, so this is always segments[last]
    in practice.

Tests:
- wal/recovery_test.go: 4 unit tests for ReplaySegmentFile covering
  last/non-last × CollectingFragments/parse-error matrix. Existing direct
  ReplaySegmentFile calls updated to pass isLastSegment=true.
- wal/recover_test.go: 4 integration tests covering middle-segment
  CollectingFragments corruption (must hard-fail), middle-segment CRC
  corruption (must hard-fail), last-segment corruption in single-segment
  WAL (must truncate), last-segment corruption in multi-segment WAL
  (must truncate only last segment).

Verified: each new test fails on pre-fix code (non-last corruption
silently truncated valid last segment) and passes after the fix. Full
suite green including go test -race ./... .

Audit context: docs/audit-3.2.md C4 (Oracle-verified bg_ef425776).
This commit is contained in:
dailz
2026-06-16 09:12:24 +08:00
parent 273229ac9b
commit 3d2d0ea025
6 changed files with 836 additions and 17 deletions
+9 -2
View File
@@ -10,12 +10,19 @@ import (
// TailCorruptionError indicates that the WAL tail contains corrupt data
// (bad CRC, unexpected non-zero padding bytes, etc.). Recovery may safely
// truncate at the last valid record.
//
// SegmentPath is set by ReplaySegmentFile when it propagates the error,
// so Recover can use it as the authoritative truncation target.
type TailCorruptionError struct {
Offset int
Err error
Offset int
SegmentPath string
Err error
}
func (e *TailCorruptionError) Error() string {
if e.SegmentPath != "" {
return fmt.Sprintf("wal: tail corruption in %s at offset %d: %v", e.SegmentPath, e.Offset, e.Err)
}
return fmt.Sprintf("wal: tail corruption at offset %d: %v", e.Offset, e.Err)
}