重要变更: - QueryOldest 和 QueryNewest 现在都返回按索引递增排序的结果 - 移除了 QueryNewest 中的结果反转操作(line 184-187) 方法行为说明: - QueryOldest(startIndex, count): 从 startIndex 向索引递增方向查询 - QueryNewest(endIndex, count): 从 endIndex 向索引递减方向查询 - 两者返回结果都按索引递增方向排序(一致性) 更新内容: 1. query.go: - 移除 QueryNewest 的反转操作 - 更新两个方法的注释 2. topic_processor.go: 更新注释与实现一致 3. seqlog_test.go: 更新测试预期结果 4. example/index/main.go: 更新注释和输出说明 测试验证: - 所有测试通过(go test ./... -short) - 示例编译成功 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
210 lines
5.2 KiB
Go
210 lines
5.2 KiB
Go
package seqlog
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
)
|
||
|
||
// RecordStatus 记录处理状态
|
||
type RecordStatus int
|
||
|
||
const (
|
||
StatusProcessed RecordStatus = iota // 已处理
|
||
StatusProcessing // 处理中(当前位置)
|
||
StatusPending // 待处理
|
||
StatusUnavailable // 不可用(尚未写入)
|
||
)
|
||
|
||
// String 返回状态的字符串表示
|
||
func (s RecordStatus) String() string {
|
||
switch s {
|
||
case StatusProcessed:
|
||
return "StatusProcessed"
|
||
case StatusProcessing:
|
||
return "StatusProcessing"
|
||
case StatusPending:
|
||
return "StatusPending"
|
||
case StatusUnavailable:
|
||
return "StatusUnavailable"
|
||
default:
|
||
return "StatusUnknown"
|
||
}
|
||
}
|
||
|
||
// RecordWithStatus 带状态的记录
|
||
type RecordWithStatus struct {
|
||
Record *Record
|
||
Status RecordStatus
|
||
}
|
||
|
||
// RecordQuery 记录查询器
|
||
type RecordQuery struct {
|
||
logPath string
|
||
fd *os.File
|
||
rbuf []byte // 复用读缓冲区
|
||
index *RecordIndex // 索引文件管理器(来自外部)
|
||
}
|
||
|
||
// NewRecordQuery 创建记录查询器
|
||
// index 参数必须由外部提供,确保所有组件使用同一个索引实例
|
||
func NewRecordQuery(logPath string, index *RecordIndex) (*RecordQuery, error) {
|
||
if index == nil {
|
||
return nil, NewValidationError("index", "index cannot be nil", ErrNilParameter)
|
||
}
|
||
|
||
fd, err := os.Open(logPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("open log file: %w", err)
|
||
}
|
||
|
||
rq := &RecordQuery{
|
||
logPath: logPath,
|
||
fd: fd,
|
||
rbuf: make([]byte, 8<<20), // 8 MiB 缓冲区
|
||
index: index,
|
||
}
|
||
|
||
return rq, nil
|
||
}
|
||
|
||
// readRecordsForward 从指定索引位置向前顺序读取记录
|
||
// startIndex: 起始记录索引
|
||
// count: 读取数量
|
||
func (rq *RecordQuery) readRecordsForward(startIndex, count int) ([]*Record, error) {
|
||
// 获取起始 offset
|
||
startOffset, err := rq.index.GetOffset(startIndex)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("get start offset: %w", err)
|
||
}
|
||
|
||
if _, err := rq.fd.Seek(startOffset, 0); err != nil {
|
||
return nil, fmt.Errorf("seek to offset %d: %w", startOffset, err)
|
||
}
|
||
|
||
results := make([]*Record, 0, count)
|
||
currentOffset := startOffset
|
||
|
||
for len(results) < count {
|
||
// 读取头部:[4B len][4B CRC][16B UUID] = 24 字节
|
||
hdr := rq.rbuf[:24]
|
||
if _, err := io.ReadFull(rq.fd, hdr); err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
return nil, fmt.Errorf("read header at offset %d: %w", currentOffset, err)
|
||
}
|
||
|
||
rec := &Record{
|
||
Len: binary.LittleEndian.Uint32(hdr[0:4]),
|
||
CRC: binary.LittleEndian.Uint32(hdr[4:8]),
|
||
}
|
||
copy(rec.UUID[:], hdr[8:24])
|
||
|
||
// 读取数据
|
||
rec.Data = make([]byte, rec.Len)
|
||
if _, err := io.ReadFull(rq.fd, rec.Data); err != nil {
|
||
return nil, fmt.Errorf("read data at offset %d: %w", currentOffset, err)
|
||
}
|
||
|
||
results = append(results, rec)
|
||
currentOffset += 24 + int64(rec.Len)
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// QueryOldest 从指定索引向索引递增方向查询记录
|
||
// startIndex: 查询起始索引
|
||
// count: 查询数量
|
||
// 返回的记录按索引递增方向排序
|
||
func (rq *RecordQuery) QueryOldest(startIndex, count int) ([]*Record, error) {
|
||
if count <= 0 {
|
||
return nil, NewValidationError("count", "count must be greater than 0", ErrInvalidCount)
|
||
}
|
||
|
||
totalCount := rq.index.Count()
|
||
if totalCount == 0 {
|
||
return []*Record{}, nil
|
||
}
|
||
|
||
// 校验起始索引
|
||
if startIndex < 0 {
|
||
startIndex = 0
|
||
}
|
||
if startIndex >= totalCount {
|
||
return []*Record{}, nil
|
||
}
|
||
|
||
// 限制查询数量
|
||
remainCount := totalCount - startIndex
|
||
if count > remainCount {
|
||
count = remainCount
|
||
}
|
||
|
||
return rq.readRecordsForward(startIndex, count)
|
||
}
|
||
|
||
// QueryNewest 从指定索引向索引递减方向查询记录
|
||
// endIndex: 查询的最大索引(向前查询更早的记录)
|
||
// count: 查询数量
|
||
// 返回的记录按索引递增方向排序(与 QueryOldest 一致)
|
||
func (rq *RecordQuery) QueryNewest(endIndex, count int) ([]*Record, error) {
|
||
if count <= 0 {
|
||
return nil, NewValidationError("count", "count must be greater than 0", ErrInvalidCount)
|
||
}
|
||
|
||
totalCount := rq.index.Count()
|
||
if totalCount == 0 {
|
||
return []*Record{}, nil
|
||
}
|
||
|
||
// 校验结束索引
|
||
if endIndex < 0 {
|
||
return []*Record{}, nil
|
||
}
|
||
if endIndex >= totalCount {
|
||
endIndex = totalCount - 1
|
||
}
|
||
|
||
// 计算实际起始索引(向索引递减方向查询 count 条)
|
||
queryStartIdx := endIndex - count + 1
|
||
if queryStartIdx < 0 {
|
||
queryStartIdx = 0
|
||
count = endIndex + 1 // 调整实际数量
|
||
}
|
||
|
||
// 向前读取,返回按索引递增排序的结果
|
||
return rq.readRecordsForward(queryStartIdx, count)
|
||
}
|
||
|
||
// GetRecordCount 获取记录总数
|
||
func (rq *RecordQuery) GetRecordCount() (int, error) {
|
||
return rq.index.Count(), nil
|
||
}
|
||
|
||
// GetRecordStatus 根据游标窗口索引位置获取记录状态
|
||
// recordIndex: 记录索引
|
||
// startIdx: 窗口开始索引(已处理位置)
|
||
// endIdx: 窗口结束索引(当前读取位置)
|
||
func GetRecordStatus(recordIndex, startIdx, endIdx int) RecordStatus {
|
||
if recordIndex < startIdx {
|
||
return StatusProcessed
|
||
} else if recordIndex >= startIdx && recordIndex < endIdx {
|
||
return StatusProcessing
|
||
} else {
|
||
return StatusPending
|
||
}
|
||
}
|
||
|
||
// Close 关闭查询器
|
||
// 注意:不关闭 index,因为 index 是外部管理的
|
||
func (rq *RecordQuery) Close() error {
|
||
// 只关闭日志文件
|
||
if rq.fd != nil {
|
||
return rq.fd.Close()
|
||
}
|
||
return nil
|
||
}
|