fix: write correct startSequence on segment rotation (C8)
SegmentManager.AppendBatch was passing sm.active.CurrentOffset() (byte
offset from file header) as the new segment's startSequence on rotation.
The result: segment-N+1's header.startSequence was a byte count (e.g.
50000), not the actual sequence number. Recovery's continuity check at
recovery.go:181-184 (segment.StartSequence != expectedSequence) failed,
making Phase 1 multi-segment recovery completely broken.
Oracle bg_ef425776 noted: "C8 是隐藏炸弹:单 segment 时一切正常,
第一次轮转后就坏".
Changes:
- wal/segment_manager.go: AppendBatch now takes batchStartSequence uint64
parameter. On rotation, passes it to rotate (which writes it to the new
segment's header.startSequence). The previous byte-offset argument is
replaced by the actual sequence number.
- wal/writer.go: processBatch passes baseSequence (already allocated by
seqManager.AllocateBatch) to AppendBatch.
- wal/segment_manager_test.go: 6 existing AppendBatch call sites updated
to pass batchStartSequence (tracked via local currentSeq variable).
Added 2 new tests:
- TestSegmentManagerRotationWritesCorrectStartSequence: verifies new
segment's header.startSequence matches the first rotated batch's
sequence (and explicitly != old byte offset, catching C8 regression).
- TestSegmentManagerMultiSegmentRecoveryRoundTrip: end-to-end test that
writes across multiple segments, closes, recovers, and verifies all
batches replay. Before C8 fix, recovery failed at continuity check.
Verified: each new test fails on pre-fix code (segment-1 startSequence
is byte offset, recovery fails) and passes after the fix. Full suite
green including go test -race ./... .
Audit context: docs/audit-3.2.md C8 (Oracle-discovered bg_2e86d33b).
This commit is contained in:
+9
-10
@@ -50,19 +50,18 @@ func NewSegmentManager(
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
// AppendBatch writes an encoded batch to the active segment. If the batch does
|
||||
// not fit in the remaining payload space (with worst-case physical record
|
||||
// overhead), the manager rotates to a fresh segment first so the entire batch
|
||||
// lands in one segment.
|
||||
func (sm *SegmentManager) AppendBatch(encodedBatch []byte) error {
|
||||
// Calculate the worst-case on-disk size for this batch:
|
||||
// len(encodedBatch) + at least one physical record header + block padding margin
|
||||
// This is a conservative upper bound. The actual overhead may be less due to
|
||||
// block alignment, but we must guarantee the batch won't exceed MaxSegmentSize.
|
||||
// AppendBatch writes encodedBatch to the active segment, rotating first if
|
||||
// the batch doesn't fit. batchStartSequence is the sequence number of the
|
||||
// FIRST entry in this batch — used as the new segment's startSequence when
|
||||
// rotation occurs, so multi-segment recovery's continuity check passes per
|
||||
// design §3.2 line 639-663.
|
||||
func (sm *SegmentManager) AppendBatch(encodedBatch []byte, batchStartSequence uint64) error {
|
||||
worstCaseSize := uint64(len(encodedBatch)) + uint64(PhysicalRecordHeaderSize) + uint64(PhysicalRecordHeaderSize)
|
||||
|
||||
if sm.active.RemainingPayload() < worstCaseSize {
|
||||
if err := sm.rotate(sm.active.CurrentOffset()); err != nil {
|
||||
// C8 fix: new segment's first batch is THIS batch, so its
|
||||
// startSequence must equal batchStartSequence (not byte offset).
|
||||
if err := sm.rotate(batchStartSequence); err != nil {
|
||||
return fmt.Errorf("wal: rotate segment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user