2026-08-15 01:36:51 +08:00

147 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package wal 实现 Agent 本地 WALWrite-Ahead Log
//
// WAL 以追加方式持久化待上报的批次,配合 checkpoint.Store 记录的已确认
// 序号,实现 Agent 进程重启后从未确认点继续上报(断点续传)。每个批次
// 使用单调递增的 seq 标识,写入顺序与发送顺序一致。
package wal
import (
"encoding/binary"
"fmt"
"io"
"os"
"sync"
)
// Record 是 WAL 中的一条记录。Payload 为已序列化的批次protobuf 字节)。
type Record struct {
Seq int64
Payload []byte
}
// WAL 是一个简单的追加式文件日志。记录格式:
//
// [seq:8字节 big-endian][len:4字节 big-endian][payload:len字节]
type WAL struct {
path string
mu sync.Mutex
f *os.File
}
// Open 打开(不存在则创建)指定路径的 WAL 文件。
func Open(path string) (*WAL, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
return nil, fmt.Errorf("wal: open %s: %w", path, err)
}
return &WAL{path: path, f: f}, nil
}
// Append 追加一条记录并立即刷盘,保证进程崩溃后可恢复。
func (w *WAL) Append(seq int64, payload []byte) error {
w.mu.Lock()
defer w.mu.Unlock()
var hdr [12]byte
binary.BigEndian.PutUint64(hdr[0:8], uint64(seq))
binary.BigEndian.PutUint32(hdr[8:12], uint32(len(payload)))
if _, err := w.f.Write(hdr[:]); err != nil {
return fmt.Errorf("wal: write header: %w", err)
}
if _, err := w.f.Write(payload); err != nil {
return fmt.Errorf("wal: write payload: %w", err)
}
if err := w.f.Sync(); err != nil {
return fmt.Errorf("wal: sync: %w", err)
}
return nil
}
// Load 读取 WAL 中的全部记录,按写入顺序返回。
func (w *WAL) Load() ([]Record, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.loadLocked()
}
func (w *WAL) loadLocked() ([]Record, error) {
if _, err := w.f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("wal: seek: %w", err)
}
var out []Record
for {
var hdr [12]byte
_, err := io.ReadFull(w.f, hdr[:])
switch {
case err == io.EOF:
return out, nil
case err == io.ErrUnexpectedEOF:
// 尾部存在半条记录(进程被强杀),视为损坏并忽略。
return out, nil
case err != nil:
return nil, fmt.Errorf("wal: read header: %w", err)
}
seq := int64(binary.BigEndian.Uint64(hdr[0:8]))
n := binary.BigEndian.Uint32(hdr[8:12])
payload := make([]byte, n)
if _, err := io.ReadFull(w.f, payload); err != nil {
return nil, fmt.Errorf("wal: read payload: %w", err)
}
out = append(out, Record{Seq: seq, Payload: payload})
}
}
// TruncateBefore 删除 seq 小于 keepSeq 的记录(即保留 seq >= keepSeq
// 通常在服务端确认后调用,以避免 WAL 无限增长。
func (w *WAL) TruncateBefore(keepSeq int64) error {
w.mu.Lock()
defer w.mu.Unlock()
recs, err := w.loadLocked()
if err != nil {
return err
}
kept := make([]Record, 0, len(recs))
for _, r := range recs {
if r.Seq >= keepSeq {
kept = append(kept, r)
}
}
if err := w.f.Truncate(0); err != nil {
return fmt.Errorf("wal: truncate: %w", err)
}
if _, err := w.f.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("wal: seek: %w", err)
}
for _, r := range kept {
var hdr [12]byte
binary.BigEndian.PutUint64(hdr[0:8], uint64(r.Seq))
binary.BigEndian.PutUint32(hdr[8:12], uint32(len(r.Payload)))
if _, err := w.f.Write(hdr[:]); err != nil {
return fmt.Errorf("wal: rewrite header: %w", err)
}
if _, err := w.f.Write(r.Payload); err != nil {
return fmt.Errorf("wal: rewrite payload: %w", err)
}
}
return w.f.Sync()
}
// Close 关闭底层文件。
func (w *WAL) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.f == nil {
return nil
}
err := w.f.Close()
w.f = nil
return err
}