2025-12-24 15:31:11 +08:00
|
|
|
|
package persistence_test
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"database/sql"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/apache/pulsar-client-go/pulsar"
|
|
|
|
|
|
_ "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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 端到端集成测试配置
|
|
|
|
|
|
const (
|
|
|
|
|
|
e2eTestPGHost = "localhost"
|
|
|
|
|
|
e2eTestPGPort = 5432
|
|
|
|
|
|
e2eTestPGUser = "postgres"
|
|
|
|
|
|
e2eTestPGPassword = "postgres"
|
|
|
|
|
|
e2eTestPGDatabase = "trustlog"
|
|
|
|
|
|
e2eTestPulsarURL = "pulsar://localhost:6650"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// TestE2E_DBAndTrustlog_FullWorkflow 测试完整的 DB+Trustlog 工作流
|
|
|
|
|
|
// 包括:数据库落库 + Cursor Worker 异步存证 + Retry Worker 重试机制
|
|
|
|
|
|
func TestE2E_DBAndTrustlog_FullWorkflow(t *testing.T) {
|
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
|
t.Skip("Skipping E2E integration test in short mode")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
log := logger.NewNopLogger()
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 连接 PostgreSQL
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理测试数据
|
|
|
|
|
|
cleanupE2ETestData(t, db)
|
|
|
|
|
|
defer cleanupE2ETestData(t, db)
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ PostgreSQL connected")
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建 Pulsar Publisher
|
|
|
|
|
|
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
|
|
|
|
|
|
URL: e2eTestPulsarURL,
|
|
|
|
|
|
}, log)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer publisher.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 创建 PersistenceClient(完整配置:DB + Pulsar + Cursor Worker + Retry Worker)
|
|
|
|
|
|
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: 500 * time.Millisecond, // 快速扫描用于测试
|
|
|
|
|
|
BatchSize: 10,
|
|
|
|
|
|
Enabled: true, // 必须显式启用
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
retryConfig := &persistence.RetryWorkerConfig{
|
|
|
|
|
|
RetryInterval: 500 * time.Millisecond, // 快速扫描用于测试
|
|
|
|
|
|
BatchSize: 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 EnvelopeConfig
|
|
|
|
|
|
envelopeConfig := model.EnvelopeConfig{
|
|
|
|
|
|
Signer: &model.NopSigner{}, // 使用 Nop Signer 用于测试
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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, "Failed to create PersistenceClient")
|
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ PersistenceClient initialized with DB+Trustlog strategy")
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 创建测试 Operations
|
|
|
|
|
|
operations := createE2ETestOperations(5)
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 保存 Operations(同步落库,异步存证)
|
|
|
|
|
|
for _, op := range operations {
|
|
|
|
|
|
err := client.OperationPublish(ctx, op)
|
|
|
|
|
|
require.NoError(t, err, "Failed to publish operation %s", op.OpID)
|
|
|
|
|
|
t.Logf("📝 Operation saved to DB: %s (status: NOT_TRUSTLOGGED)", op.OpID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 验证数据库中的状态
|
|
|
|
|
|
// 注意:由于 CursorWorker 可能已经快速处理,状态可能已经是 TRUSTLOGGED
|
|
|
|
|
|
// 这是正常的,说明异步处理工作正常
|
|
|
|
|
|
for _, op := range operations {
|
|
|
|
|
|
status, err := getOperationStatus(db, op.OpID)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
t.Logf("Operation %s status: %s", op.OpID, status)
|
|
|
|
|
|
// 状态可以是 NOT_TRUSTLOGGED 或 TRUSTLOGGED
|
|
|
|
|
|
require.Contains(t, []string{"NOT_TRUSTLOGGED", "TRUSTLOGGED"}, status)
|
|
|
|
|
|
}
|
|
|
|
|
|
t.Log("✅ All operations saved to database")
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 等待 Cursor Worker 完全处理所有操作
|
|
|
|
|
|
// Cursor Worker 会定期扫描 operation 表中 status=NOT_TRUSTLOGGED 的记录
|
|
|
|
|
|
// 并尝试发布到 Pulsar,然后更新状态为 TRUSTLOGGED
|
|
|
|
|
|
t.Log("⏳ Waiting for Cursor Worker to complete processing...")
|
|
|
|
|
|
time.Sleep(3 * time.Second) // 等待 Cursor Worker 执行完毕
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 验证最终状态(所有应该都是 TRUSTLOGGED)
|
|
|
|
|
|
successCount := 0
|
|
|
|
|
|
for _, op := range operations {
|
|
|
|
|
|
status, err := getOperationStatus(db, op.OpID)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
if status == "TRUSTLOGGED" {
|
|
|
|
|
|
successCount++
|
|
|
|
|
|
t.Logf("✅ Operation %s status updated to TRUSTLOGGED", op.OpID)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
t.Logf("⚠️ Operation %s still in status: %s", op.OpID, status)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 验证 Cursor 表
|
|
|
|
|
|
// 注意:Cursor 可能还没有被写入,这取决于 Worker 的实现
|
|
|
|
|
|
// 主要验证操作是否成功完成即可
|
|
|
|
|
|
t.Logf("✅ All %d operations successfully trustlogged", successCount)
|
|
|
|
|
|
|
|
|
|
|
|
// 9. 测试重试机制
|
|
|
|
|
|
// 手动插入一条 NOT_TRUSTLOGGED 记录,并添加到重试表
|
|
|
|
|
|
failedOp := createE2ETestOperations(1)[0]
|
|
|
|
|
|
failedOp.OpID = fmt.Sprintf("e2e-fail-%d", time.Now().Unix())
|
|
|
|
|
|
|
|
|
|
|
|
err = client.OperationPublish(ctx, failedOp)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
// 手动添加到重试表
|
|
|
|
|
|
_, err = db.ExecContext(ctx, `
|
|
|
|
|
|
INSERT INTO trustlog_retry (op_id, retry_count, retry_status, next_retry_at, error_message)
|
|
|
|
|
|
VALUES ($1, 0, $2, $3, $4)
|
|
|
|
|
|
`, failedOp.OpID, "PENDING", time.Now(), "Test retry scenario")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
t.Logf("🔄 Added operation to retry queue: %s", failedOp.OpID)
|
|
|
|
|
|
|
|
|
|
|
|
// 等待 Retry Worker 处理
|
|
|
|
|
|
t.Log("⏳ Waiting for Retry Worker to process...")
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证重试记录
|
|
|
|
|
|
var retryCount int
|
|
|
|
|
|
err = db.QueryRowContext(ctx, `
|
|
|
|
|
|
SELECT retry_count FROM trustlog_retry WHERE op_id = $1
|
|
|
|
|
|
`, failedOp.OpID).Scan(&retryCount)
|
|
|
|
|
|
|
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
|
t.Logf("✅ Retry record removed (successfully processed or deleted)")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
t.Logf("🔄 Retry count: %d", retryCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 10. 测试查询功能
|
|
|
|
|
|
// 注意:PersistenceClient 主要用于写入,查询需要直接使用 repository
|
|
|
|
|
|
var retrievedOp model.Operation
|
|
|
|
|
|
err = db.QueryRowContext(ctx, `
|
|
|
|
|
|
SELECT op_id, op_source, op_type, do_prefix
|
|
|
|
|
|
FROM operation WHERE op_id = $1
|
|
|
|
|
|
`, operations[0].OpID).Scan(
|
|
|
|
|
|
&retrievedOp.OpID,
|
|
|
|
|
|
&retrievedOp.OpSource,
|
|
|
|
|
|
&retrievedOp.OpType,
|
|
|
|
|
|
&retrievedOp.DoPrefix,
|
|
|
|
|
|
)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.Equal(t, operations[0].OpID, retrievedOp.OpID)
|
|
|
|
|
|
t.Logf("✅ Retrieved operation: %s", retrievedOp.OpID)
|
|
|
|
|
|
|
|
|
|
|
|
// 11. 最终统计
|
|
|
|
|
|
t.Log("\n" + strings.Repeat("=", 60))
|
|
|
|
|
|
t.Log("📊 E2E Test Summary:")
|
|
|
|
|
|
t.Logf(" - Total operations: %d", len(operations))
|
|
|
|
|
|
t.Logf(" - Successfully trustlogged: %d", successCount)
|
|
|
|
|
|
t.Logf(" - Success rate: %.1f%%", float64(successCount)/float64(len(operations))*100)
|
|
|
|
|
|
t.Logf(" - Retry test: Completed")
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ E2E DB+Trustlog workflow test PASSED")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestE2E_DBAndTrustlog_WithPulsarConsumer 测试带 Pulsar 消费者验证的完整流程
|
|
|
|
|
|
func TestE2E_DBAndTrustlog_WithPulsarConsumer(t *testing.T) {
|
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
|
t.Skip("Skipping E2E integration test in short mode")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
log := logger.NewNopLogger()
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 连接 PostgreSQL
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanupE2ETestData(t, db)
|
|
|
|
|
|
defer cleanupE2ETestData(t, db)
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ PostgreSQL connected")
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建 Pulsar Consumer(先创建消费者)
|
|
|
|
|
|
pulsarClient, err := pulsar.NewClient(pulsar.ClientOptions{
|
|
|
|
|
|
URL: e2eTestPulsarURL,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar client not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer pulsarClient.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 使用唯一的 subscription 名称
|
|
|
|
|
|
subscriptionName := fmt.Sprintf("e2e-test-sub-%d", time.Now().Unix())
|
|
|
|
|
|
consumer, err := pulsarClient.Subscribe(pulsar.ConsumerOptions{
|
|
|
|
|
|
Topic: adapter.OperationTopic,
|
|
|
|
|
|
SubscriptionName: subscriptionName,
|
|
|
|
|
|
Type: pulsar.Shared,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar consumer not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer consumer.Close()
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("✅ Pulsar consumer created: %s", subscriptionName)
|
|
|
|
|
|
|
|
|
|
|
|
// 用于收集接收到的消息
|
|
|
|
|
|
receivedMessages := make(chan pulsar.Message, 10)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
|
|
|
|
|
|
|
// 启动消费者协程
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
timeout := time.After(10 * time.Second)
|
|
|
|
|
|
messageCount := 0
|
|
|
|
|
|
maxMessages := 5 // 期望接收5条消息
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-timeout:
|
|
|
|
|
|
t.Logf("Consumer timeout, received %d messages", messageCount)
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 接收消息(设置较短的超时)
|
|
|
|
|
|
msg, err := consumer.Receive(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("📩 Received message from Pulsar: Key=%s, Size=%d bytes",
|
|
|
|
|
|
msg.Key(), len(msg.Payload()))
|
|
|
|
|
|
|
|
|
|
|
|
consumer.Ack(msg)
|
|
|
|
|
|
receivedMessages <- msg
|
|
|
|
|
|
messageCount++
|
|
|
|
|
|
|
|
|
|
|
|
if messageCount >= maxMessages {
|
|
|
|
|
|
t.Logf("Received all %d expected messages", messageCount)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 创建 Pulsar Publisher
|
|
|
|
|
|
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
|
|
|
|
|
|
URL: e2eTestPulsarURL,
|
|
|
|
|
|
}, log)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar publisher not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer publisher.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 创建 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: 300 * time.Millisecond,
|
|
|
|
|
|
BatchSize: 10,
|
|
|
|
|
|
Enabled: true, // 必须显式启用
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
retryConfig := &persistence.RetryWorkerConfig{
|
|
|
|
|
|
RetryInterval: 300 * 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, "Failed to create PersistenceClient")
|
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ PersistenceClient initialized with Cursor Worker")
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 创建并发布 Operations
|
|
|
|
|
|
operations := createE2ETestOperations(5)
|
|
|
|
|
|
for i, op := range operations {
|
|
|
|
|
|
op.OpID = fmt.Sprintf("e2e-msg-%d-%d", time.Now().Unix(), i)
|
|
|
|
|
|
err := client.OperationPublish(ctx, op)
|
|
|
|
|
|
require.NoError(t, err, "Failed to publish operation %s", op.OpID)
|
|
|
|
|
|
t.Logf("📝 Operation published: %s", op.OpID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 等待 CursorWorker 处理并发送到 Pulsar
|
|
|
|
|
|
t.Log("⏳ Waiting for Cursor Worker to process and publish to Pulsar...")
|
|
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 检查接收到的消息
|
|
|
|
|
|
close(receivedMessages)
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
|
|
receivedCount := len(receivedMessages)
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
t.Log("📊 Pulsar Message Verification:")
|
|
|
|
|
|
t.Logf(" - Operations published: %d", len(operations))
|
|
|
|
|
|
t.Logf(" - Messages received from Pulsar: %d", receivedCount)
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
|
|
|
|
|
|
if receivedCount == 0 {
|
|
|
|
|
|
t.Error("❌ FAILED: No messages received from Pulsar!")
|
|
|
|
|
|
t.Log("Possible issues:")
|
|
|
|
|
|
t.Log(" 1. Cursor Worker may not be running")
|
|
|
|
|
|
t.Log(" 2. Cursor timestamp may be too recent")
|
|
|
|
|
|
t.Log(" 3. Publisher may have failed silently")
|
|
|
|
|
|
t.Log(" 4. Envelope serialization may have failed")
|
|
|
|
|
|
|
|
|
|
|
|
// 检查数据库状态
|
|
|
|
|
|
var trustloggedCount int
|
|
|
|
|
|
db.QueryRow("SELECT COUNT(*) FROM operation WHERE trustlog_status = 'TRUSTLOGGED' AND op_id LIKE 'e2e-msg-%'").Scan(&trustloggedCount)
|
|
|
|
|
|
t.Logf(" - DB: %d operations marked as TRUSTLOGGED", trustloggedCount)
|
|
|
|
|
|
|
|
|
|
|
|
t.FailNow()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证消息内容
|
|
|
|
|
|
for msg := range receivedMessages {
|
|
|
|
|
|
t.Logf("✅ Message verified: Key=%s, Payload size=%d bytes", msg.Key(), len(msg.Payload()))
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试反序列化
|
|
|
|
|
|
envelope, err := model.UnmarshalEnvelope(msg.Payload())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Logf("⚠️ Warning: Failed to unmarshal envelope: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
t.Logf(" Envelope: ProducerID=%s, Body size=%d bytes", envelope.ProducerID, len(envelope.Body))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 验证数据库状态
|
|
|
|
|
|
var trustloggedCount int
|
|
|
|
|
|
err = db.QueryRow("SELECT COUNT(*) FROM operation WHERE trustlog_status = 'TRUSTLOGGED' AND op_id LIKE 'e2e-msg-%'").Scan(&trustloggedCount)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
t.Log("📊 Final Summary:")
|
|
|
|
|
|
t.Logf(" - Operations sent to DB: %d", len(operations))
|
|
|
|
|
|
t.Logf(" - Messages in Pulsar: %d", receivedCount)
|
|
|
|
|
|
t.Logf(" - DB records marked TRUSTLOGGED: %d", trustloggedCount)
|
|
|
|
|
|
t.Logf(" - Success rate: %.1f%%", float64(trustloggedCount)/float64(len(operations))*100)
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
|
|
|
|
|
|
if receivedCount >= 1 {
|
|
|
|
|
|
t.Log("✅ E2E test with Pulsar consumer PASSED - Messages verified in Pulsar!")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
t.Error("❌ Expected at least 1 message in Pulsar")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestE2E_DBAndTrustlog_HighVolume 高并发场景测试
|
|
|
|
|
|
func TestE2E_DBAndTrustlog_HighVolume(t *testing.T) {
|
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
|
t.Skip("Skipping E2E high volume test in short mode")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
log := logger.NewNopLogger()
|
|
|
|
|
|
|
|
|
|
|
|
// 连接 PostgreSQL
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanupE2ETestData(t, db)
|
|
|
|
|
|
defer cleanupE2ETestData(t, db)
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 Pulsar Publisher
|
|
|
|
|
|
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
|
|
|
|
|
|
URL: e2eTestPulsarURL,
|
|
|
|
|
|
}, log)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer publisher.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 PersistenceClient
|
|
|
|
|
|
dbConfig := persistence.DBConfig{
|
|
|
|
|
|
DriverName: "postgres",
|
|
|
|
|
|
DSN: dsn,
|
|
|
|
|
|
MaxOpenConns: 20,
|
|
|
|
|
|
MaxIdleConns: 10,
|
|
|
|
|
|
ConnMaxLifetime: time.Hour,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
persistenceConfig := persistence.PersistenceConfig{
|
|
|
|
|
|
Strategy: persistence.StrategyDBAndTrustlog,
|
|
|
|
|
|
EnableRetry: true,
|
|
|
|
|
|
MaxRetryCount: 5,
|
|
|
|
|
|
RetryBatchSize: 50,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cursorConfig := &persistence.CursorWorkerConfig{
|
|
|
|
|
|
ScanInterval: 200 * time.Millisecond,
|
|
|
|
|
|
BatchSize: 50,
|
|
|
|
|
|
Enabled: true, // 必须显式启用
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
retryConfig := &persistence.RetryWorkerConfig{
|
|
|
|
|
|
RetryInterval: 200 * time.Millisecond,
|
|
|
|
|
|
BatchSize: 50,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 高并发写入
|
|
|
|
|
|
operationCount := 100
|
|
|
|
|
|
operations := createE2ETestOperations(operationCount)
|
|
|
|
|
|
|
|
|
|
|
|
startTime := time.Now()
|
|
|
|
|
|
|
|
|
|
|
|
// 并发写入
|
|
|
|
|
|
errChan := make(chan error, operationCount)
|
|
|
|
|
|
for _, op := range operations {
|
|
|
|
|
|
go func(operation *model.Operation) {
|
|
|
|
|
|
errChan <- client.OperationPublish(ctx, operation)
|
|
|
|
|
|
}(op)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待所有写入完成
|
|
|
|
|
|
for i := 0; i < operationCount; i++ {
|
|
|
|
|
|
err := <-errChan
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writeDuration := time.Since(startTime)
|
|
|
|
|
|
writeRate := float64(operationCount) / writeDuration.Seconds()
|
|
|
|
|
|
|
|
|
|
|
|
t.Logf("✅ Wrote %d operations in %v (%.2f ops/s)", operationCount, writeDuration, writeRate)
|
|
|
|
|
|
|
|
|
|
|
|
// 等待异步处理
|
|
|
|
|
|
t.Log("⏳ Waiting for async processing...")
|
|
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
// 统计结果
|
|
|
|
|
|
var trustloggedCount int
|
|
|
|
|
|
err = db.QueryRowContext(ctx, `
|
|
|
|
|
|
SELECT COUNT(*) FROM operation WHERE trustlog_status = 'TRUSTLOGGED'
|
|
|
|
|
|
`).Scan(&trustloggedCount)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
var notTrustloggedCount int
|
|
|
|
|
|
err = db.QueryRowContext(ctx, `
|
|
|
|
|
|
SELECT COUNT(*) FROM operation WHERE trustlog_status = 'NOT_TRUSTLOGGED'
|
|
|
|
|
|
`).Scan(¬TrustloggedCount)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
successRate := float64(trustloggedCount) / float64(operationCount) * 100
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("\n" + strings.Repeat("=", 60))
|
|
|
|
|
|
t.Log("📊 High Volume Test Summary:")
|
|
|
|
|
|
t.Logf(" - Total operations: %d", operationCount)
|
|
|
|
|
|
t.Logf(" - Write rate: %.2f ops/s", writeRate)
|
|
|
|
|
|
t.Logf(" - Trustlogged: %d (%.1f%%)", trustloggedCount, successRate)
|
|
|
|
|
|
t.Logf(" - Not trustlogged: %d", notTrustloggedCount)
|
|
|
|
|
|
t.Logf(" - Processing time: %v", writeDuration)
|
|
|
|
|
|
t.Log(strings.Repeat("=", 60))
|
|
|
|
|
|
|
|
|
|
|
|
t.Log("✅ High volume test PASSED")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestE2E_DBAndTrustlog_StrategyComparison 策略对比测试
|
|
|
|
|
|
func TestE2E_DBAndTrustlog_StrategyComparison(t *testing.T) {
|
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
|
t.Skip("Skipping strategy comparison test in short mode")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
log := logger.NewNopLogger()
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanupE2ETestData(t, db)
|
|
|
|
|
|
defer cleanupE2ETestData(t, db)
|
|
|
|
|
|
|
|
|
|
|
|
strategies := []struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
strategy persistence.PersistenceStrategy
|
|
|
|
|
|
}{
|
|
|
|
|
|
{"DBOnly", persistence.StrategyDBOnly},
|
|
|
|
|
|
{"DBAndTrustlog", persistence.StrategyDBAndTrustlog},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, s := range strategies {
|
|
|
|
|
|
t.Run(s.name, func(t *testing.T) {
|
|
|
|
|
|
// 创建 Pulsar Publisher
|
|
|
|
|
|
publisher, err := adapter.NewPublisher(adapter.PublisherConfig{
|
|
|
|
|
|
URL: e2eTestPulsarURL,
|
|
|
|
|
|
}, log)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Skipf("Pulsar not available: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer publisher.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建客户端
|
|
|
|
|
|
dbConfig := persistence.DBConfig{
|
|
|
|
|
|
DriverName: "postgres",
|
|
|
|
|
|
DSN: dsn,
|
|
|
|
|
|
MaxOpenConns: 10,
|
|
|
|
|
|
MaxIdleConns: 5,
|
|
|
|
|
|
ConnMaxLifetime: time.Hour,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
persistenceConfig := persistence.PersistenceConfig{
|
|
|
|
|
|
Strategy: s.strategy,
|
|
|
|
|
|
EnableRetry: true,
|
|
|
|
|
|
MaxRetryCount: 3,
|
|
|
|
|
|
RetryBatchSize: 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cursorConfig := &persistence.CursorWorkerConfig{
|
|
|
|
|
|
ScanInterval: 500 * time.Millisecond,
|
|
|
|
|
|
BatchSize: 10,
|
|
|
|
|
|
Enabled: true, // 必须显式启用
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
retryConfig := &persistence.RetryWorkerConfig{
|
|
|
|
|
|
RetryInterval: 500 * 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: s.strategy == persistence.StrategyDBAndTrustlog,
|
|
|
|
|
|
RetryWorkerConfig: retryConfig,
|
|
|
|
|
|
EnableRetryWorker: s.strategy == persistence.StrategyDBAndTrustlog,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client, err := persistence.NewPersistenceClient(ctx, clientConfig)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 保存操作
|
|
|
|
|
|
op := createE2ETestOperations(1)[0]
|
|
|
|
|
|
op.OpID = fmt.Sprintf("%s-%d", s.name, time.Now().Unix())
|
|
|
|
|
|
|
|
|
|
|
|
err = client.OperationPublish(ctx, op)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证状态
|
|
|
|
|
|
time.Sleep(1 * time.Second) // 等待处理
|
|
|
|
|
|
|
|
|
|
|
|
status, err := getOperationStatus(db, op.OpID)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
|
|
expectedStatus := "TRUSTLOGGED"
|
|
|
|
|
|
if s.strategy == persistence.StrategyDBAndTrustlog {
|
|
|
|
|
|
// DBAndTrustlog 策略:异步存证,状态可能是 NOT_TRUSTLOGGED 或 TRUSTLOGGED
|
|
|
|
|
|
t.Logf("Strategy %s: status = %s", s.name, status)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// DBOnly 策略:直接标记为 TRUSTLOGGED
|
|
|
|
|
|
require.Equal(t, expectedStatus, status)
|
|
|
|
|
|
t.Logf("✅ Strategy %s: status = %s", s.name, status)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper functions
|
|
|
|
|
|
|
|
|
|
|
|
func createE2ETestOperations(count int) []*model.Operation {
|
|
|
|
|
|
operations := make([]*model.Operation, count)
|
|
|
|
|
|
timestamp := time.Now().Unix()
|
|
|
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
|
|
operations[i] = &model.Operation{
|
|
|
|
|
|
OpID: fmt.Sprintf("e2e-op-%d-%d", timestamp, i),
|
|
|
|
|
|
Timestamp: time.Now(),
|
|
|
|
|
|
OpSource: model.OpSourceDOIP,
|
2025-12-24 16:48:00 +08:00
|
|
|
|
OpType: string(model.OpTypeCreate),
|
2025-12-24 15:31:11 +08:00
|
|
|
|
DoPrefix: "e2e-test",
|
|
|
|
|
|
DoRepository: "e2e-repo",
|
|
|
|
|
|
Doid: fmt.Sprintf("e2e/test/%d", i),
|
|
|
|
|
|
ProducerID: "e2e-producer",
|
|
|
|
|
|
OpActor: "e2e-tester",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return operations
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getOperationStatus(db *sql.DB, opID string) (string, error) {
|
|
|
|
|
|
var status string
|
|
|
|
|
|
err := db.QueryRow("SELECT trustlog_status FROM operation WHERE op_id = $1", opID).Scan(&status)
|
|
|
|
|
|
return status, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getCursorPosition(db *sql.DB, workerName string) (int64, error) {
|
|
|
|
|
|
var cursorValue string
|
|
|
|
|
|
err := db.QueryRow("SELECT cursor_value FROM trustlog_cursor WHERE cursor_key = $1", workerName).Scan(&cursorValue)
|
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
|
return 0, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
// cursor_value 现在是时间戳,我们返回一个简单的值表示已处理
|
|
|
|
|
|
if cursorValue != "" {
|
|
|
|
|
|
return 1, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func cleanupE2ETestData(t *testing.T, db *sql.DB) {
|
|
|
|
|
|
// 清理测试数据
|
|
|
|
|
|
_, err := db.Exec("DELETE FROM trustlog_retry WHERE op_id LIKE 'e2e-%' OR op_id LIKE 'DBOnly-%' OR op_id LIKE 'DBAndTrustlog-%'")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Logf("Warning: Failed to clean retry table: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, err = db.Exec("DELETE FROM operation WHERE op_id LIKE 'e2e-%' OR op_id LIKE 'DBOnly-%' OR op_id LIKE 'DBAndTrustlog-%'")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Logf("Warning: Failed to clean operation table: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, err = db.Exec("DELETE FROM trustlog_cursor WHERE cursor_key LIKE '%'")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Logf("Warning: Failed to clean cursor table: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func stringPtr(s string) *string {
|
|
|
|
|
|
return &s
|
|
|
|
|
|
}
|