ccpoad / internal /storage /sql /transaction_deadline_test.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
5.24 kB
package sql
import (
"context"
"database/sql"
"errors"
"sync"
"testing"
"time"
_ "modernc.org/sqlite"
)
// TestWithTransaction_ContextDeadline 验证 context.Deadline 限制总重试时间
// [FIX] 后续优化: 防止事务重试超过 context 的 deadline
func TestWithTransaction_ContextDeadline(t *testing.T) {
t.Run("context 有 deadline 时应该提前退出", func(t *testing.T) {
// 创建临时数据库
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("打开数据库失败: %v", err)
}
defer func() { _ = db.Close() }()
// 创建一个 500ms deadline 的 context
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
attemptCount := 0
start := time.Now()
// 模拟一个总是返回 BUSY 错误的事务
err = withTransaction(ctx, db, func(_ *sql.Tx) error {
attemptCount++
// 模拟 SQLite BUSY 错误
return errors.New("database is locked")
})
elapsed := time.Since(start)
// 验证:应该在 deadline 前退出(不是等到 12 次重试完)
if err == nil {
t.Fatal("期望失败,但成功了")
}
// 验证:耗时应该接近 500ms,而不是 51.2s(12 次重试的理论最大值)
if elapsed > 1*time.Second {
t.Errorf("重试耗时过长: %v(应该在 deadline 前退出)", elapsed)
}
// 验证:应该有多次重试(至少 2-3 次)
if attemptCount < 2 {
t.Errorf("重试次数过少: %d(应该至少有几次重试)", attemptCount)
}
// 验证:不应该达到最大重试次数 12
if attemptCount >= 12 {
t.Errorf("重试次数过多: %d(应该在 deadline 前退出)", attemptCount)
}
t.Logf("✅ context.Deadline 生效: 耗时 %v, 重试 %d 次后提前退出", elapsed, attemptCount)
})
t.Run("没有 deadline 时应该正常重试到最大次数", func(t *testing.T) {
// 创建临时数据库
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("打开数据库失败: %v", err)
}
defer func() { _ = db.Close() }()
// 使用 background context(无 deadline)
ctx := context.Background()
attemptCount := 0
// 模拟一个总是返回 BUSY 错误的事务
err = withTransaction(ctx, db, func(_ *sql.Tx) error {
attemptCount++
return errors.New("database is locked")
})
// 验证:应该重试到最大次数
if attemptCount != 12 {
t.Errorf("重试次数不符合预期: got %d, want 12", attemptCount)
}
// 验证:错误信息应该包含"after 12 retries"
if err == nil || err.Error() == "" {
t.Fatal("期望失败,但成功了或错误为空")
}
t.Logf("✅ 无 deadline 时正常重试到最大次数: %d 次", attemptCount)
})
t.Run("context 取消时应该立即退出", func(t *testing.T) {
// 创建临时数据库
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("打开数据库失败: %v", err)
}
defer func() { _ = db.Close() }()
// 创建可取消的 context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
attemptCount := 0
start := time.Now()
firstAttempt := make(chan struct{})
var closeOnce sync.Once
closeFirstAttempt := func() { closeOnce.Do(func() { close(firstAttempt) }) }
go func() {
<-firstAttempt
cancel()
}()
// 模拟一个总是返回 BUSY 错误的事务
err = withTransaction(ctx, db, func(_ *sql.Tx) error {
attemptCount++
if attemptCount == 1 {
closeFirstAttempt()
}
return errors.New("database is locked")
})
closeFirstAttempt()
elapsed := time.Since(start)
// 验证:应该快速退出(不是等到 12 次重试完)
if elapsed > 500*time.Millisecond {
t.Errorf("取消后耗时过长: %v", elapsed)
}
// 验证:错误信息应该包含"cancelled"
if err == nil {
t.Fatal("期望失败,但成功了")
}
t.Logf("✅ context 取消时立即退出: 耗时 %v, 重试 %d 次", elapsed, attemptCount)
})
}
// TestWithTransaction_DeadlineRealWorld 模拟真实的 deadline 场景
func TestWithTransaction_DeadlineRealWorld(t *testing.T) {
t.Run("HTTP 请求超时应该传播到事务层", func(t *testing.T) {
// 创建临时数据库
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("打开数据库失败: %v", err)
}
defer func() { _ = db.Close() }()
// 模拟 HTTP 请求的 1 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
attemptCount := 0
start := time.Now()
// 模拟事务操作(总是失败)
err = withTransaction(ctx, db, func(_ *sql.Tx) error {
attemptCount++
return errors.New("database is deadlocked")
})
if err == nil {
t.Fatal("期望事务失败,但成功了")
}
elapsed := time.Since(start)
// 验证:应该在 1 秒左右退出
if elapsed > 1500*time.Millisecond {
t.Errorf("超时控制失效: 耗时 %v(应该约 1s)", elapsed)
}
// 验证:不应该达到 12 次重试
if attemptCount >= 12 {
t.Errorf("重试次数过多: %d(应该被 deadline 提前终止)", attemptCount)
}
t.Logf("✅ HTTP 超时传播到事务层: 耗时 %v, 重试 %d 次后退出", elapsed, attemptCount)
})
}