Files
seqlog/cursor.go
bourdon bcc328b129 重构:TopicProcessor 状态管理系统与 Reset 方法优化
新增功能:
- 添加 ProcessorState 状态类型(Idle/Starting/Running/Stopping/Stopped/Resetting/Error)
- 添加 ProcessorStatus 结构体和状态管理方法(GetState/GetStatus/setState)
- 实现状态转换逻辑和访问控制(CanWrite/CanQuery)
- 新增 CanReset() 方法检查是否可执行重置操作

Reset 方法优化:
- 重写 Reset() 方法,不再停止 processor
- 只有在无待处理记录时才能执行重置
- 进入 Resetting 状态期间阻止所有读写操作
- 重置后自动恢复到之前的运行状态
- 正确关闭并重置 cursor 和 stats 组件
- 调整执行顺序:先关闭组件,再删除文件,后重新初始化

错误处理增强:
- 添加 ErrProcessorResetting 和 ErrInvalidState 错误类型
- 添加 EventStateChanged 事件类型
- 修复 writer/index 为 nil 时的空指针问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 18:56:52 +08:00

212 lines
5.5 KiB
Go
Raw 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 seqlog
import (
"encoding/binary"
"fmt"
"hash/crc32"
"io"
"os"
"github.com/google/uuid"
)
// ProcessCursor 日志游标(窗口模式)
type ProcessCursor struct {
fd *os.File
rbuf []byte // 8 MiB 复用
path string // 日志文件路径
posFile string // 游标位置文件路径
startIdx int // 窗口开始索引(已处理的记录索引)
endIdx int // 窗口结束索引(当前读到的记录索引)
index *RecordIndex // 索引管理器(来自外部)
writer *LogWriter // 写入器引用(用于检查写入位置)
}
// NewCursor 创建一个新的日志游标
// index: 外部提供的索引管理器,用于快速定位记录
// writer: 外部提供的写入器引用,用于检查写入位置(可选,为 nil 时不进行写入保护检查)
func NewCursor(path string, index *RecordIndex, writer *LogWriter) (*ProcessCursor, error) {
if index == nil {
return nil, NewValidationError("index", "index cannot be nil", ErrNilParameter)
}
fd, err := os.Open(path)
if err != nil {
return nil, err
}
c := &ProcessCursor{
fd: fd,
rbuf: make([]byte, 8<<20),
path: path,
posFile: path + ".pos",
startIdx: 0,
endIdx: 0,
index: index,
writer: writer,
}
// 尝试恢复上次位置
c.loadPosition()
return c, nil
}
// Seek 到任意 offset支持重启续传
func (c *ProcessCursor) Seek(offset int64, whence int) (int64, error) {
return c.fd.Seek(offset, whence)
}
// Next 读取下一条记录(使用索引快速定位)
func (c *ProcessCursor) Next() (*Record, error) {
// 检查是否超出索引范围
if c.endIdx >= c.index.Count() {
return nil, io.EOF
}
// 从索引获取当前记录的偏移量
offset, err := c.index.GetOffset(c.endIdx)
if err != nil {
return nil, fmt.Errorf("get offset from index: %w", err)
}
// 写入保护:检查读取位置是否超过当前写入位置
if c.writer != nil {
dirtyOffset := c.writer.GetDirtyOffset()
// 如果正在写入dirtyOffset >= 0且记录起始位置 >= 写入位置,说明数据还未完全写入,返回 EOF 等待
if dirtyOffset >= 0 && offset >= dirtyOffset {
return nil, io.EOF
}
}
// Seek 到记录位置
if _, err := c.fd.Seek(offset, 0); err != nil {
return nil, fmt.Errorf("seek to offset %d: %w", offset, err)
}
// 读取头部:[4B len][8B offset][4B CRC][16B UUID] = 32 字节
hdr := c.rbuf[:32]
if _, err := io.ReadFull(c.fd, hdr); err != nil {
return nil, err
}
var rec Record
rec.Len = binary.LittleEndian.Uint32(hdr[0:4])
// hdr[4:12] 是 offset读取时不需要使用
rec.CRC = binary.LittleEndian.Uint32(hdr[12:16])
// 读取并校验 UUID
copy(rec.UUID[:], hdr[16:32])
if _, err := uuid.FromBytes(rec.UUID[:]); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidUUID, err)
}
// 如果数据大于缓冲区,分配新的 buffer
var payload []byte
if int(rec.Len) <= len(c.rbuf)-32 {
payload = c.rbuf[32 : 32+rec.Len]
} else {
payload = make([]byte, rec.Len)
}
if _, err := io.ReadFull(c.fd, payload); err != nil {
return nil, err
}
if crc32.ChecksumIEEE(payload) != rec.CRC {
return nil, fmt.Errorf("%w: offset=%d", ErrCRCMismatch, offset)
}
rec.Data = append([]byte(nil), payload...) // 复制出去,复用 buffer
// 更新窗口结束索引(移动到下一条记录)
c.endIdx++
return &rec, nil
}
// NextRange 读取指定数量的记录(范围游动)
// count: 要读取的记录数量
// 返回:读取到的记录列表,如果到达文件末尾,返回的记录数可能少于 count
func (c *ProcessCursor) NextRange(count int) ([]*Record, error) {
if count <= 0 {
return nil, NewValidationError("count", "count must be greater than 0", ErrInvalidCount)
}
results := make([]*Record, 0, count)
for range count {
rec, err := c.Next()
if err != nil {
if err == io.EOF && len(results) > 0 {
// 已经读取了一些记录,返回这些记录
return results, nil
}
return results, err
}
results = append(results, rec)
}
return results, nil
}
// Commit 提交窗口,将 endIdx 移动到 startIdx表示已处理完这批记录
func (c *ProcessCursor) Commit() {
c.startIdx = c.endIdx
}
// Rollback 回滚窗口,将 endIdx 回退到 startIdx表示放弃这批记录的处理
func (c *ProcessCursor) Rollback() error {
c.endIdx = c.startIdx
return nil
}
// StartIndex 获取窗口开始索引
func (c *ProcessCursor) StartIndex() int {
return c.startIdx
}
// EndIndex 获取窗口结束索引
func (c *ProcessCursor) EndIndex() int {
return c.endIdx
}
// Close 关闭游标并保存位置
func (c *ProcessCursor) Close() error {
c.savePosition()
return c.fd.Close()
}
// savePosition 保存当前读取位置到文件
func (c *ProcessCursor) savePosition() error {
f, err := os.Create(c.posFile)
if err != nil {
return err
}
defer f.Close()
buf := make([]byte, 4)
// 保存 startIdx已处理的索引
binary.LittleEndian.PutUint32(buf, uint32(c.startIdx))
_, err = f.Write(buf)
return err
}
// loadPosition 从文件加载上次的读取位置
func (c *ProcessCursor) loadPosition() error {
f, err := os.Open(c.posFile)
if err != nil {
if os.IsNotExist(err) {
return nil // 文件不存在,从头开始
}
return err
}
defer f.Close()
buf := make([]byte, 4)
if _, err := io.ReadFull(f, buf); err != nil {
return err
}
// 加载 startIdx
c.startIdx = int(binary.LittleEndian.Uint32(buf))
c.endIdx = c.startIdx
return nil
}