package wal import ( "errors" "fmt" "math" ) // BatchReplayer is the interface for replaying recovered WAL entries. // The recovery process calls the appropriate method for each entry. type BatchReplayer interface { ReplayPut(key, value []byte, sequence uint64) ReplayDelete(key []byte, sequence uint64) } type replayAction struct { opType uint8 key []byte value []byte sequence uint64 } // ReplayBatch validates and replays a decoded WAL batch, returning the next // expected sequence after the batch. func ReplayBatch(batch *WalBatch, expectedSequence uint64, replayer BatchReplayer) (nextSequence uint64, err error) { if batch == nil { return expectedSequence, errors.New("wal: batch is nil") } if replayer == nil { return expectedSequence, errors.New("wal: batch replayer is nil") } if batch.Flags != 0 { return expectedSequence, fmt.Errorf("wal: invalid batch flags %d", batch.Flags) } if batch.EntryCount == 0 { return expectedSequence, errors.New("wal: batch entry count is zero") } if batch.EntryCount > MaxWalBatchEntryCount { return expectedSequence, fmt.Errorf("wal: batch entry count %d exceeds maximum %d", batch.EntryCount, MaxWalBatchEntryCount) } if batch.EntriesSize != uint32(len(batch.Entries)) { return expectedSequence, fmt.Errorf("wal: batch entries size mismatch: header says %d bytes, have %d bytes", batch.EntriesSize, len(batch.Entries)) } if batch.EntriesSize == 0 { return expectedSequence, errors.New("wal: batch entries size is zero") } if batch.EntriesSize > MaxWalBatchEntriesSize { return expectedSequence, fmt.Errorf("wal: batch entries size %d exceeds maximum %d", batch.EntriesSize, MaxWalBatchEntriesSize) } if batch.BaseSequence != expectedSequence { return expectedSequence, fmt.Errorf("wal: batch base sequence %d does not match expected sequence %d", batch.BaseSequence, expectedSequence) } entryCount := uint64(batch.EntryCount) if batch.BaseSequence > math.MaxUint64-(entryCount-1) { return expectedSequence, fmt.Errorf("wal: batch sequence range overflows uint64: base sequence %d entry count %d", batch.BaseSequence, batch.EntryCount) } if expectedSequence > math.MaxUint64-entryCount { return expectedSequence, fmt.Errorf("wal: next sequence overflows uint64: expected sequence %d entry count %d", expectedSequence, batch.EntryCount) } actions := make([]replayAction, 0, batch.EntryCount) offset := 0 for i := range entryCount { entry, consumed, err := DecodeEntry(batch.Entries[offset:]) if err != nil { return expectedSequence, fmt.Errorf("wal: decode batch entry: %w", err) } if consumed <= 0 { return expectedSequence, errors.New("wal: decoded batch entry consumed no bytes") } sequence := batch.BaseSequence + i actions = append(actions, replayAction{ opType: entry.OpType, key: entry.Key, value: entry.Value, sequence: sequence, }) offset += consumed } if offset != len(batch.Entries) { return expectedSequence, fmt.Errorf("wal: batch entries contain trailing bytes: parsed %d of %d", offset, len(batch.Entries)) } for _, action := range actions { switch action.opType { case OpPut: replayer.ReplayPut(action.key, action.value, action.sequence) case OpDelete: replayer.ReplayDelete(action.key, action.sequence) default: return expectedSequence, fmt.Errorf("wal: invalid op type %d", action.opType) } } return expectedSequence + entryCount, nil } // ReplaySegmentFile replays all complete WAL batches from one segment file. func ReplaySegmentFile(filePath string, startSequence uint64, replayer BatchReplayer) (nextSequence uint64, err error) { nextSequence = startSequence records, parseErr := ParseRecordsFromFile(filePath) if parseErr != nil && !IsTailCorruption(parseErr) { return nextSequence, fmt.Errorf("wal: parse segment records: %w", parseErr) } collector := NewFragmentCollector() for _, record := range records { if err := collector.Append(record.Type, record.Payload); err != nil { return nextSequence, fmt.Errorf("wal: collect segment fragments: %w", err) } if !collector.IsComplete() { continue } batch, err := DecodeWalBatch(collector.BatchData()) if err != nil { return nextSequence, fmt.Errorf("wal: decode recovered batch: %w", err) } nextSequence, err = ReplayBatch(batch, nextSequence, replayer) if err != nil { return nextSequence, fmt.Errorf("wal: replay recovered batch: %w", err) } collector.Reset() } if parseErr != nil { return nextSequence, parseErr } if collector.State() == FragmentCollecting { return nextSequence, &TailCorruptionError{ Offset: 0, Err: errors.New("incomplete fragmented batch at segment tail"), } } return nextSequence, nil } // RecoverFromSegments scans and replays WAL segments from recoverySegmentID. func RecoverFromSegments(dir string, recoverySegmentID uint64, replayer BatchReplayer) (nextSequence uint64, err error) { segments, err := ScanSegments(dir, recoverySegmentID) if err != nil { return 0, fmt.Errorf("wal: scan recovery segments: %w", err) } if len(segments) == 0 { return 0, nil } nextSequence = segments[0].StartSequence for _, segment := range segments { if segment.StartSequence != nextSequence { return nextSequence, fmt.Errorf("wal: segment start sequence %d does not match expected sequence %d", segment.StartSequence, nextSequence) } nextSequence, err = ReplaySegmentFile(segment.FilePath, nextSequence, replayer) if err != nil { if IsTailCorruption(err) { return nextSequence, err } return nextSequence, fmt.Errorf("wal: replay segment: %w", err) } } return nextSequence, nil }