重构:优化记录格式并修复核心功能

- 修改记录存储格式为 [4B len][8B offset][4B CRC][16B UUID][data]
- 修复 TopicProcessor 中 WaitGroup 使用错误导致 handler 不执行的问题
- 修复写入保护逻辑,避免 dirtyOffset=-1 时误判为写入中
- 添加统计信息定期持久化功能
- 改进 UTF-8 字符截断处理,防止 CJK 字符乱码
- 优化 Web UI:显示人类可读的文件大小,支持点击外部关闭弹窗
- 重构示例代码,添加 webui 和 webui_integration 示例

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 17:54:49 +08:00
parent 955a467248
commit 810664eb12
18 changed files with 1810 additions and 1170 deletions

View File

@@ -19,11 +19,13 @@ type ProcessCursor struct {
startIdx int // 窗口开始索引(已处理的记录索引)
endIdx int // 窗口结束索引(当前读到的记录索引)
index *RecordIndex // 索引管理器(来自外部)
writer *LogWriter // 写入器引用(用于检查写入位置)
}
// NewCursor 创建一个新的日志游标
// index: 外部提供的索引管理器,用于快速定位记录
func NewCursor(path string, index *RecordIndex) (*ProcessCursor, error) {
// writer: 外部提供的写入器引用,用于检查写入位置(可选,为 nil 时不进行写入保护检查)
func NewCursor(path string, index *RecordIndex, writer *LogWriter) (*ProcessCursor, error) {
if index == nil {
return nil, NewValidationError("index", "index cannot be nil", ErrNilParameter)
}
@@ -40,6 +42,7 @@ func NewCursor(path string, index *RecordIndex) (*ProcessCursor, error) {
startIdx: 0,
endIdx: 0,
index: index,
writer: writer,
}
// 尝试恢复上次位置
c.loadPosition()
@@ -64,31 +67,39 @@ func (c *ProcessCursor) Next() (*Record, error) {
return nil, fmt.Errorf("get offset from index: %w", err)
}
// 写入保护:检查读取位置是否超过当前写入位置
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][4B CRC][16B UUID] = 24 字节
hdr := c.rbuf[:24]
// 读取头部:[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])
rec.CRC = binary.LittleEndian.Uint32(hdr[4:8])
// hdr[4:12] 是 offset读取时不需要使用
rec.CRC = binary.LittleEndian.Uint32(hdr[12:16])
// 读取并校验 UUID
copy(rec.UUID[:], hdr[8:24])
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)-24 {
payload = c.rbuf[24 : 24+rec.Len]
if int(rec.Len) <= len(c.rbuf)-32 {
payload = c.rbuf[32 : 32+rec.Len]
} else {
payload = make([]byte, rec.Len)
}
@@ -97,7 +108,7 @@ func (c *ProcessCursor) Next() (*Record, error) {
return nil, err
}
if crc32.ChecksumIEEE(payload) != rec.CRC {
return nil, ErrCRCMismatch
return nil, fmt.Errorf("%w: offset=%d", ErrCRCMismatch, offset)
}
rec.Data = append([]byte(nil), payload...) // 复制出去,复用 buffer