Files
seqlog/cursor.go
bourdon 90cc9e21c9 重构:重命名核心组件并增强查询功能
主要更改:

1. 核心重命名
   - Seqlog -> LogHub (更准确地反映其作为日志中枢的角色)
   - NewSeqlog() -> NewLogHub()
   - LogCursor -> ProcessCursor (更准确地反映其用于处理场景)
   - seqlog_manager.go -> loghub.go (文件名与结构体名对应)

2. TopicProcessor.Reset 增强
   - 如果正在运行且没有待处理的日志,会自动停止后重置
   - 如果有待处理的日志,返回详细错误(显示已处理/总记录数)
   - 简化了 LogHub.ResetTopic,移除显式 Stop 调用

3. 新增查询方法
   - TopicProcessor.QueryFromFirst(count) - 从第一条记录向索引递增方向查询
   - TopicProcessor.QueryFromLast(count) - 从最后一条记录向索引递减方向查询
   - LogHub.QueryFromFirst(topic, count)
   - LogHub.QueryFromLast(topic, count)

4. 测试覆盖
   - 添加 query_test.go - QueryFromProcessing 测试
   - 添加 TestQueryFromFirstAndLast - TopicProcessor 查询测试
   - 添加 TestLogHubQueryFromFirstAndLast - LogHub 查询测试
   - 添加 TestTopicResetWithPendingRecords - Reset 增强功能测试

5. 示例代码
   - 添加 example/get_record/ - 演示 QueryFromProcessing 用法
   - 更新所有示例以使用 LogHub 和新 API

所有测试通过 

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:26:21 +08:00

199 lines
4.8 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 // 索引管理器(来自外部)
}
// NewCursor 创建一个新的日志游标
// index: 外部提供的索引管理器,用于快速定位记录
func NewCursor(path string, index *RecordIndex) (*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,
}
// 尝试恢复上次位置
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)
}
// 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("%w: %v", ErrInvalidUUID, 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, ErrCRCMismatch
}
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
}