重构:统一使用索引(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>
2025-10-03 23:48:21 +08:00
|
|
|
|
package seqlog
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// TestIndexBasicOperations 测试索引的基本操作
|
|
|
|
|
|
func TestIndexBasicOperations(t *testing.T) {
|
|
|
|
|
|
// 创建临时目录
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 创建索引
|
|
|
|
|
|
index, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer index.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建写入器(使用共享索引)
|
|
|
|
|
|
writer, err := NewLogWriter(logPath, index)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 写入测试数据
|
|
|
|
|
|
testData := []string{
|
|
|
|
|
|
"第一条日志",
|
|
|
|
|
|
"第二条日志",
|
|
|
|
|
|
"第三条日志",
|
|
|
|
|
|
"第四条日志",
|
|
|
|
|
|
"第五条日志",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
offsets := make([]int64, 0, len(testData))
|
|
|
|
|
|
for _, data := range testData {
|
|
|
|
|
|
offset, err := writer.Append([]byte(data))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("写入失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
offsets = append(offsets, offset)
|
|
|
|
|
|
t.Logf("写入记录: offset=%d, data=%s", offset, data)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭写入器
|
|
|
|
|
|
if err := writer.Close(); err != nil {
|
|
|
|
|
|
t.Fatalf("关闭写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 验证索引文件存在
|
|
|
|
|
|
indexPath := logPath + ".idx"
|
|
|
|
|
|
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
|
|
|
|
|
|
t.Fatalf("索引文件不存在: %s", indexPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
t.Logf("索引文件已创建: %s", indexPath)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证记录数量
|
|
|
|
|
|
if index.Count() != len(testData) {
|
|
|
|
|
|
t.Errorf("记录数量不匹配: got %d, want %d", index.Count(), len(testData))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证每条记录的偏移量
|
|
|
|
|
|
for i, expectedOffset := range offsets {
|
|
|
|
|
|
actualOffset, err := index.GetOffset(i)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Errorf("获取第 %d 条记录的偏移失败: %v", i, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if actualOffset != expectedOffset {
|
|
|
|
|
|
t.Errorf("第 %d 条记录偏移量不匹配: got %d, want %d", i, actualOffset, expectedOffset)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证二分查找
|
|
|
|
|
|
for i, offset := range offsets {
|
|
|
|
|
|
idx := index.FindIndex(offset)
|
|
|
|
|
|
if idx != i {
|
|
|
|
|
|
t.Errorf("FindIndex(%d) = %d, want %d", offset, idx, i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("索引基本操作测试通过")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestIndexRebuild 测试索引重建功能
|
|
|
|
|
|
func TestIndexRebuild(t *testing.T) {
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 创建索引和写入器,写入数据
|
|
|
|
|
|
index1, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writer, err := NewLogWriter(logPath, index1)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
offsets := make([]int64, 0, 3)
|
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
|
offset, err := writer.Append([]byte("测试数据"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("写入失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
offsets = append(offsets, offset)
|
|
|
|
|
|
}
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
index1.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 重新加载索引(测试索引加载功能)
|
|
|
|
|
|
index, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer index.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 验证重建的索引
|
|
|
|
|
|
if index.Count() != 3 {
|
|
|
|
|
|
t.Errorf("重建的索引记录数不正确: got %d, want 3", index.Count())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for i, expectedOffset := range offsets {
|
|
|
|
|
|
actualOffset, err := index.GetOffset(i)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Errorf("获取偏移失败: %v", err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if actualOffset != expectedOffset {
|
|
|
|
|
|
t.Errorf("偏移量不匹配: got %d, want %d", actualOffset, expectedOffset)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("索引重建测试通过")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestQueryWithIndex 测试带索引的查询
|
|
|
|
|
|
func TestQueryWithIndex(t *testing.T) {
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
|
|
|
|
|
|
|
|
// 创建索引
|
|
|
|
|
|
index, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer index.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建写入器(使用共享索引)
|
|
|
|
|
|
writer, err := NewLogWriter(logPath, index)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入 10 条记录
|
|
|
|
|
|
for range 10 {
|
|
|
|
|
|
_, err := writer.Append([]byte("测试数据"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("写入失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建查询器(使用共享索引)
|
|
|
|
|
|
query, err := NewRecordQuery(logPath, index)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建查询器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer query.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 测试获取记录总数
|
|
|
|
|
|
count, err := query.GetRecordCount()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("获取记录总数失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if count != 10 {
|
|
|
|
|
|
t.Errorf("记录总数不正确: got %d, want 10", count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 00:10:14 +08:00
|
|
|
|
// 5. 测试向后查询(查询更早的记录)
|
|
|
|
|
|
// 从第 5 条记录向后查询 3 条(查询索引 4, 3, 2)
|
|
|
|
|
|
results, err := query.QueryNewest(4, 3)
|
重构:统一使用索引(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>
2025-10-03 23:48:21 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("向后查询失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(results) != 3 {
|
|
|
|
|
|
t.Errorf("查询结果数量不正确: got %d, want 3", len(results))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("带索引的查询测试通过")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestIndexAppend 测试索引追加功能
|
|
|
|
|
|
func TestIndexAppend(t *testing.T) {
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 创建索引和写入器,写入初始数据
|
|
|
|
|
|
index1, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writer, err := NewLogWriter(logPath, index1)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for range 5 {
|
|
|
|
|
|
writer.Append([]byte("初始数据"))
|
|
|
|
|
|
}
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
index1.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 重新打开索引和写入器,追加新数据
|
|
|
|
|
|
index2, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("重新打开索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writer, err = NewLogWriter(logPath, index2)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("重新打开写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for range 3 {
|
|
|
|
|
|
writer.Append([]byte("追加数据"))
|
|
|
|
|
|
}
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
index2.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 验证索引
|
|
|
|
|
|
index, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("加载索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer index.Close()
|
|
|
|
|
|
|
|
|
|
|
|
if index.Count() != 8 {
|
|
|
|
|
|
t.Errorf("索引记录数不正确: got %d, want 8", index.Count())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("索引追加测试通过")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestIndexHeader 测试索引头部信息
|
|
|
|
|
|
func TestIndexHeader(t *testing.T) {
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
|
|
|
|
|
|
|
|
// 创建索引
|
|
|
|
|
|
index, err := NewRecordIndex(logPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建索引失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer index.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建写入器(使用共享索引)
|
|
|
|
|
|
writer, err := NewLogWriter(logPath, index)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建写入器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastOffset, _ := writer.Append([]byte("第一条"))
|
|
|
|
|
|
writer.Append([]byte("第二条"))
|
|
|
|
|
|
lastOffset, _ = writer.Append([]byte("第三条"))
|
|
|
|
|
|
writer.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 验证魔数和版本
|
2025-10-03 23:55:18 +08:00
|
|
|
|
if index.magic != IndexMagic {
|
|
|
|
|
|
t.Errorf("Magic 不正确: got 0x%X, want 0x%X", index.magic, IndexMagic)
|
重构:统一使用索引(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>
2025-10-03 23:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 23:55:18 +08:00
|
|
|
|
if index.version != IndexVersion {
|
|
|
|
|
|
t.Errorf("Version 不正确: got %d, want %d", index.version, IndexVersion)
|
重构:统一使用索引(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>
2025-10-03 23:48:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证记录总数(从内存索引计算)
|
|
|
|
|
|
if index.Count() != 3 {
|
|
|
|
|
|
t.Errorf("Count 不正确: got %d, want 3", index.Count())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证最后一条记录偏移(从内存索引获取)
|
|
|
|
|
|
if index.LastOffset() != lastOffset {
|
|
|
|
|
|
t.Errorf("LastOffset 不正确: got %d, want %d", index.LastOffset(), lastOffset)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("索引头部信息测试通过")
|
|
|
|
|
|
}
|