feat: 完善数据库持久化与存证功能

主要更新:

1. 数据库持久化功能
   - 支持三种策略:仅落库、既落库又存证、仅存证
   - 实现 Cursor Worker 异步扫描和存证机制
   - 实现 Retry Worker 失败重试机制
   - 支持 PostgreSQL、MySQL、SQLite 等多种数据库
   - 添加 ClientIP 和 ServerIP 字段(可空,仅落库)

2. 集群并发安全
   - 使用 SELECT FOR UPDATE SKIP LOCKED 防止重复处理
   - 实现 CAS (Compare-And-Set) 原子状态更新
   - 添加 updated_at 字段支持并发控制

3. Cursor 初始化优化
   - 自动基于历史数据初始化 cursor
   - 确保不遗漏任何历史记录
   - 修复 UPSERT 逻辑

4. 测试完善
   - 添加 E2E 集成测试(含 Pulsar 消费者验证)
   - 添加 PostgreSQL 集成测试
   - 添加 Pulsar 集成测试
   - 添加集群并发安全测试
   - 添加 Cursor 初始化验证测试
   - 补充大量单元测试,提升覆盖率

5. 工具脚本
   - 添加数据库迁移脚本
   - 添加 Cursor 状态检查工具
   - 添加 Cursor 初始化工具
   - 添加 Pulsar 消息验证工具

6. 文档清理
   - 删除冗余文档,只保留根目录 README

测试结果:
- 所有 E2E 测试通过(100%)
- 数据库持久化与异步存证流程验证通过
- 集群环境下的并发安全性验证通过
- Cursor 自动初始化和历史数据处理验证通过
This commit is contained in:
ryan
2025-12-24 15:31:11 +08:00
parent 88f80ffa5e
commit 4b72a37120
60 changed files with 6160 additions and 1313 deletions

View File

@@ -0,0 +1,283 @@
package persistence_test
import (
"context"
"database/sql"
"fmt"
"strings"
"testing"
"time"
_ "github.com/lib/pq"
"github.com/stretchr/testify/require"
"go.yandata.net/iod/iod/go-trustlog/api/adapter"
"go.yandata.net/iod/iod/go-trustlog/api/logger"
"go.yandata.net/iod/iod/go-trustlog/api/model"
"go.yandata.net/iod/iod/go-trustlog/api/persistence"
)
// TestCursorInitialization 验证 cursor 初始化逻辑
func TestCursorInitialization(t *testing.T) {
if testing.Short() {
t.Skip("Skipping cursor initialization test in short mode")
}
ctx := context.Background()
log := logger.NewDefaultLogger() // 使用标准logger来输出诊断信息
// 连接数据库
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
e2eTestPGHost, e2eTestPGPort, e2eTestPGUser, e2eTestPGPassword, e2eTestPGDatabase)
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Skipf("PostgreSQL not available: %v", err)
return
}
defer db.Close()
if err := db.Ping(); err != nil {
t.Skipf("PostgreSQL not reachable: %v", err)
return
}
// 清理测试数据
_, _ = db.Exec("DELETE FROM trustlog_retry WHERE op_id LIKE 'cursor-init-%'")
_, _ = db.Exec("DELETE FROM operation WHERE op_id LIKE 'cursor-init-%'")
_, _ = db.Exec("DELETE FROM trustlog_cursor")
defer func() {
_, _ = db.Exec("DELETE FROM trustlog_retry WHERE op_id LIKE 'cursor-init-%'")
_, _ = db.Exec("DELETE FROM operation WHERE op_id LIKE 'cursor-init-%'")
_, _ = db.Exec("DELETE FROM trustlog_cursor")
}()
t.Log("✅ PostgreSQL connected and cleaned")
// 场景 1: 没有历史数据时启动
t.Run("NoHistoricalData", func(t *testing.T) {
// 清理
_, _ = db.Exec("DELETE FROM operation")
_, _ = db.Exec("DELETE FROM trustlog_cursor")
// 创建 Pulsar Publisher
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
URL: e2eTestPulsarURL,
}, log)
require.NoError(t, err)
defer publisher.Close()
// 创建 PersistenceClient
dbConfig := persistence.DBConfig{
DriverName: "postgres",
DSN: dsn,
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: time.Hour,
}
persistenceConfig := persistence.PersistenceConfig{
Strategy: persistence.StrategyDBAndTrustlog,
EnableRetry: true,
MaxRetryCount: 3,
RetryBatchSize: 10,
}
cursorConfig := &persistence.CursorWorkerConfig{
ScanInterval: 100 * time.Millisecond,
BatchSize: 10,
Enabled: true, // 必须显式启用!
}
retryConfig := &persistence.RetryWorkerConfig{
RetryInterval: 100 * time.Millisecond,
BatchSize: 10,
}
envelopeConfig := model.EnvelopeConfig{
Signer: &model.NopSigner{},
}
clientConfig := persistence.PersistenceClientConfig{
Publisher: publisher,
Logger: log,
EnvelopeConfig: envelopeConfig,
DBConfig: dbConfig,
PersistenceConfig: persistenceConfig,
CursorWorkerConfig: cursorConfig,
EnableCursorWorker: true,
RetryWorkerConfig: retryConfig,
EnableRetryWorker: true,
}
client, err := persistence.NewPersistenceClient(ctx, clientConfig)
require.NoError(t, err)
// 等待初始化
time.Sleep(500 * time.Millisecond)
// 验证 cursor 已创建
var cursorValue string
var updatedAt time.Time
err = db.QueryRow("SELECT cursor_value, last_updated_at FROM trustlog_cursor WHERE cursor_key = 'operation_scan'").Scan(&cursorValue, &updatedAt)
require.NoError(t, err, "❌ Cursor should be initialized!")
t.Logf("✅ Cursor initialized: %s", cursorValue)
t.Logf(" Updated at: %v", updatedAt)
// cursor 应该是一个很早的时间(因为没有历史数据)
cursorTime, err := time.Parse(time.RFC3339Nano, cursorValue)
require.NoError(t, err)
require.True(t, cursorTime.Before(time.Now().Add(-1*time.Hour)), "Cursor should be set to an early time")
client.Close()
})
// 场景 2: 有历史数据时启动
t.Run("WithHistoricalData", func(t *testing.T) {
// 清理
_, _ = db.Exec("DELETE FROM operation WHERE op_id LIKE 'cursor-init-%'")
_, _ = db.Exec("DELETE FROM trustlog_cursor")
// 插入一些历史数据
baseTime := time.Now().Add(-10 * time.Minute)
for i := 0; i < 5; i++ {
opID := fmt.Sprintf("cursor-init-%d", i)
createdAt := baseTime.Add(time.Duration(i) * time.Minute)
_, err := db.Exec(`
INSERT INTO operation (
op_id, op_actor, doid, producer_id,
request_body_hash, response_body_hash, op_hash, sign,
op_source, op_type, do_prefix, do_repository,
trustlog_status, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`, opID, "tester", fmt.Sprintf("test/%d", i), "producer",
"req-hash", "resp-hash", "op-hash", "signature",
"DOIP", "CREATE", "test", "repo", "NOT_TRUSTLOGGED", createdAt)
require.NoError(t, err)
}
t.Logf("✅ Created 5 historical records starting from %v", baseTime)
// 创建 Pulsar Publisher
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
URL: e2eTestPulsarURL,
}, log)
require.NoError(t, err)
defer publisher.Close()
// 创建 PersistenceClient
dbConfig := persistence.DBConfig{
DriverName: "postgres",
DSN: dsn,
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: time.Hour,
}
persistenceConfig := persistence.PersistenceConfig{
Strategy: persistence.StrategyDBAndTrustlog,
EnableRetry: true,
MaxRetryCount: 3,
RetryBatchSize: 10,
}
cursorConfig := &persistence.CursorWorkerConfig{
ScanInterval: 100 * time.Millisecond,
BatchSize: 10,
Enabled: true, // 必须显式启用!
}
retryConfig := &persistence.RetryWorkerConfig{
RetryInterval: 100 * time.Millisecond,
BatchSize: 10,
}
envelopeConfig := model.EnvelopeConfig{
Signer: &model.NopSigner{},
}
clientConfig := persistence.PersistenceClientConfig{
Publisher: publisher,
Logger: log,
EnvelopeConfig: envelopeConfig,
DBConfig: dbConfig,
PersistenceConfig: persistenceConfig,
CursorWorkerConfig: cursorConfig,
EnableCursorWorker: true,
RetryWorkerConfig: retryConfig,
EnableRetryWorker: true,
}
t.Log("📌 Creating PersistenceClient...")
t.Logf(" EnableCursorWorker: %v", clientConfig.EnableCursorWorker)
t.Logf(" Strategy: %v", clientConfig.PersistenceConfig.Strategy)
client, err := persistence.NewPersistenceClient(ctx, clientConfig)
require.NoError(t, err)
t.Log("✅ PersistenceClient created")
// 立即验证初始 cursor (在 Worker 开始扫描前)
// 注意:由于 Worker 可能已经开始处理,我们需要快速读取
time.Sleep(10 * time.Millisecond) // 给一点时间让 InitCursor 完成
var initialCursorValue string
var updatedAt time.Time
err = db.QueryRow("SELECT cursor_value, last_updated_at FROM trustlog_cursor WHERE cursor_key = 'operation_scan'").Scan(&initialCursorValue, &updatedAt)
require.NoError(t, err, "❌ Cursor should be initialized!")
t.Logf("📍 Initial cursor: %s", initialCursorValue)
t.Logf(" Updated at: %v", updatedAt)
// 验证初始 cursor 应该在最早记录之前(或接近)
initialCursorTime, err := time.Parse(time.RFC3339Nano, initialCursorValue)
require.NoError(t, err)
var earliestRecordTime time.Time
err = db.QueryRow("SELECT MIN(created_at) FROM operation WHERE op_id LIKE 'cursor-init-%'").Scan(&earliestRecordTime)
require.NoError(t, err)
t.Logf(" Earliest record: %v", earliestRecordTime)
t.Logf(" Initial cursor time: %v", initialCursorTime)
// cursor 应该在最早记录之前或相差不超过2秒考虑 Worker 可能已经开始更新)
timeDiff := earliestRecordTime.Sub(initialCursorTime)
require.True(t, timeDiff >= -2*time.Second,
"❌ Cursor (%v) should be before or near earliest record (%v), diff: %v",
initialCursorTime, earliestRecordTime, timeDiff)
t.Log("✅ Initial cursor position is correct!")
// 等待 Worker 处理所有记录
t.Log("⏳ Waiting for Worker to process all records...")
time.Sleep(3 * time.Second)
// 再次查询 cursor看看是否被更新
var updatedCursorValue string
var finalUpdatedAt time.Time
err = db.QueryRow("SELECT cursor_value, last_updated_at FROM trustlog_cursor WHERE cursor_key = 'operation_scan'").Scan(&updatedCursorValue, &finalUpdatedAt)
require.NoError(t, err)
t.Logf("📍 Cursor after processing:")
t.Logf(" Value: %s", updatedCursorValue)
t.Logf(" Updated: %v", finalUpdatedAt)
t.Logf(" Changed: %v", updatedCursorValue != initialCursorValue)
// 验证所有记录都被处理了
var trustloggedCount int
err = db.QueryRow("SELECT COUNT(*) FROM operation WHERE op_id LIKE 'cursor-init-%' AND trustlog_status = 'TRUSTLOGGED'").Scan(&trustloggedCount)
require.NoError(t, err)
t.Logf("📊 Processed records: %d/5", trustloggedCount)
require.Equal(t, 5, trustloggedCount, "❌ All 5 records should be processed!")
t.Log("✅ All historical records were processed correctly!")
client.Close()
})
t.Log("\n" + strings.Repeat("=", 60))
t.Log("✅ Cursor initialization verification PASSED")
t.Log(strings.Repeat("=", 60))
}