| package sql |
|
|
| import ( |
| "context" |
| "database/sql" |
| "errors" |
| "sync" |
| "testing" |
| "time" |
|
|
| _ "modernc.org/sqlite" |
| ) |
|
|
| |
| |
| 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() }() |
|
|
| |
| ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) |
| defer cancel() |
|
|
| attemptCount := 0 |
| start := time.Now() |
|
|
| |
| err = withTransaction(ctx, db, func(_ *sql.Tx) error { |
| attemptCount++ |
| |
| return errors.New("database is locked") |
| }) |
|
|
| elapsed := time.Since(start) |
|
|
| |
| if err == nil { |
| t.Fatal("期望失败,但成功了") |
| } |
|
|
| |
| if elapsed > 1*time.Second { |
| t.Errorf("重试耗时过长: %v(应该在 deadline 前退出)", elapsed) |
| } |
|
|
| |
| if attemptCount < 2 { |
| t.Errorf("重试次数过少: %d(应该至少有几次重试)", attemptCount) |
| } |
|
|
| |
| 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() }() |
|
|
| |
| ctx := context.Background() |
|
|
| attemptCount := 0 |
|
|
| |
| 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) |
| } |
|
|
| |
| 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() }() |
|
|
| |
| 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() |
| }() |
|
|
| |
| err = withTransaction(ctx, db, func(_ *sql.Tx) error { |
| attemptCount++ |
| if attemptCount == 1 { |
| closeFirstAttempt() |
| } |
| return errors.New("database is locked") |
| }) |
| closeFirstAttempt() |
|
|
| elapsed := time.Since(start) |
|
|
| |
| if elapsed > 500*time.Millisecond { |
| t.Errorf("取消后耗时过长: %v", elapsed) |
| } |
|
|
| |
| if err == nil { |
| t.Fatal("期望失败,但成功了") |
| } |
|
|
| t.Logf("✅ context 取消时立即退出: 耗时 %v, 重试 %d 次", elapsed, attemptCount) |
| }) |
| } |
|
|
| |
| 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() }() |
|
|
| |
| 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) |
|
|
| |
| if elapsed > 1500*time.Millisecond { |
| t.Errorf("超时控制失效: 耗时 %v(应该约 1s)", elapsed) |
| } |
|
|
| |
| if attemptCount >= 12 { |
| t.Errorf("重试次数过多: %d(应该被 deadline 提前终止)", attemptCount) |
| } |
|
|
| t.Logf("✅ HTTP 超时传播到事务层: 耗时 %v, 重试 %d 次后退出", elapsed, attemptCount) |
| }) |
| } |
|
|