重构:统一使用索引(Index)替代位置(Position)进行状态判断
## 主要变更 ### 架构改进 - 明确索引(Index)与偏移(Offset)的职责分离 - Index: 记录序号(逻辑概念),用于状态判断 - Offset: 文件字节位置(物理概念),仅用于 I/O 操作 ### API 变更 - 删除所有 Position 相关方法: - `LogCursor.StartPos()/EndPos()` - `LogTailer.GetStartPos()/GetEndPos()` - `TopicProcessor.GetProcessingPosition()/GetReadPosition()` - `Seqlog.GetProcessingPosition()/GetReadPosition()` - 新增索引方法: - `LogCursor.StartIndex()/EndIndex()` - `LogTailer.GetStartIndex()/GetEndIndex()` - `TopicProcessor.GetProcessingIndex()/GetReadIndex()` - `Seqlog.GetProcessingIndex()/GetReadIndex()` - `Seqlog.GetProcessor()` - 获取 processor 实例以访问 Index ### 查询接口变更 - `RecordQuery.QueryOldest(startIndex, count, startIdx, endIdx)` - 使用索引参数 - `RecordQuery.QueryNewest(endIndex, count, startIdx, endIdx)` - 使用索引参数 - `RecordQuery.QueryAt(position, direction, count, startIdx, endIdx)` - startIdx/endIdx 用于状态判断 ### 性能优化 - 状态判断改用整数比较,不再需要计算偏移量 - 减少不必要的索引到偏移的转换 - 只在实际文件 I/O 时才获取 offset ### 测试更新 - 更新所有测试用例使用新的 Index API - 更新示例代码(topic_processor_example.go, webapp/main.go) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
198
cursor.go
Normal file
198
cursor.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package seqlog
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LogCursor 日志游标(窗口模式)
|
||||
type LogCursor struct {
|
||||
fd *os.File
|
||||
rbuf []byte // 8 MiB 复用
|
||||
path string // 日志文件路径
|
||||
posFile string // 游标位置文件路径
|
||||
startIdx int // 窗口开始索引(已处理的记录索引)
|
||||
endIdx int // 窗口结束索引(当前读到的记录索引)
|
||||
index *RecordIndex // 索引管理器(来自外部)
|
||||
}
|
||||
|
||||
// NewCursor 创建一个新的日志游标
|
||||
// index: 外部提供的索引管理器,用于快速定位记录
|
||||
func NewCursor(path string, index *RecordIndex) (*LogCursor, error) {
|
||||
if index == nil {
|
||||
return nil, fmt.Errorf("index cannot be nil")
|
||||
}
|
||||
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &LogCursor{
|
||||
fd: fd,
|
||||
rbuf: make([]byte, 8<<20),
|
||||
path: path,
|
||||
posFile: path + ".pos",
|
||||
startIdx: 0,
|
||||
endIdx: 0,
|
||||
index: index,
|
||||
}
|
||||
// 尝试恢复上次位置
|
||||
c.loadPosition()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Seek 到任意 offset(支持重启续传)
|
||||
func (c *LogCursor) Seek(offset int64, whence int) (int64, error) {
|
||||
return c.fd.Seek(offset, whence)
|
||||
}
|
||||
|
||||
// Next 读取下一条记录(使用索引快速定位)
|
||||
func (c *LogCursor) 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)
|
||||
}
|
||||
|
||||
// Seek 到记录位置
|
||||
if _, err := c.fd.Seek(offset, 0); err != nil {
|
||||
return nil, fmt.Errorf("seek to offset %d: %w", offset, err)
|
||||
}
|
||||
|
||||
// 读取头部:[4B len][4B CRC][16B UUID] = 24 字节
|
||||
hdr := c.rbuf[:24]
|
||||
if _, err := io.ReadFull(c.fd, hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rec Record
|
||||
rec.Len = binary.LittleEndian.Uint32(hdr[0:4])
|
||||
rec.CRC = binary.LittleEndian.Uint32(hdr[4:8])
|
||||
|
||||
// 读取并校验 UUID
|
||||
copy(rec.UUID[:], hdr[8:24])
|
||||
if _, err := uuid.FromBytes(rec.UUID[:]); err != nil {
|
||||
return nil, fmt.Errorf("invalid uuid: %w", err)
|
||||
}
|
||||
|
||||
// 如果数据大于缓冲区,分配新的 buffer
|
||||
var payload []byte
|
||||
if int(rec.Len) <= len(c.rbuf)-24 {
|
||||
payload = c.rbuf[24 : 24+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("crc mismatch")
|
||||
}
|
||||
rec.Data = append([]byte(nil), payload...) // 复制出去,复用 buffer
|
||||
|
||||
// 更新窗口结束索引(移动到下一条记录)
|
||||
c.endIdx++
|
||||
|
||||
return &rec, nil
|
||||
}
|
||||
|
||||
// NextRange 读取指定数量的记录(范围游动)
|
||||
// count: 要读取的记录数量
|
||||
// 返回:读取到的记录列表,如果到达文件末尾,返回的记录数可能少于 count
|
||||
func (c *LogCursor) NextRange(count int) ([]*Record, error) {
|
||||
if count <= 0 {
|
||||
return nil, fmt.Errorf("count must be greater than 0")
|
||||
}
|
||||
|
||||
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 *LogCursor) Commit() {
|
||||
c.startIdx = c.endIdx
|
||||
}
|
||||
|
||||
// Rollback 回滚窗口,将 endIdx 回退到 startIdx(表示放弃这批记录的处理)
|
||||
func (c *LogCursor) Rollback() error {
|
||||
c.endIdx = c.startIdx
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartIndex 获取窗口开始索引
|
||||
func (c *LogCursor) StartIndex() int {
|
||||
return c.startIdx
|
||||
}
|
||||
|
||||
// EndIndex 获取窗口结束索引
|
||||
func (c *LogCursor) EndIndex() int {
|
||||
return c.endIdx
|
||||
}
|
||||
|
||||
// Close 关闭游标并保存位置
|
||||
func (c *LogCursor) Close() error {
|
||||
c.savePosition()
|
||||
return c.fd.Close()
|
||||
}
|
||||
|
||||
// savePosition 保存当前读取位置到文件
|
||||
func (c *LogCursor) 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 *LogCursor) 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
|
||||
}
|
||||
Reference in New Issue
Block a user