package resilience import ( "context" "errors" "sync/atomic" "testing" "time" ) var errDown = errors.New("下游 503") func TestRetrySucceedsAfterTransientErrors(t *testing.T) { var calls atomic.Int32 err := Retry(context.Background(), 5, time.Millisecond, func(context.Context) error { if calls.Add(1) < 3 { return errDown } return nil }) if err != nil || calls.Load() != 3 { t.Fatalf("err=%v calls=%d", err, calls.Load()) } } func TestRetryStopsOnPermanent(t *testing.T) { var calls int err := Retry(context.Background(), 5, time.Millisecond, func(context.Context) error { calls++ return Permanent{errors.New("参数错误")} }) if calls != 1 || err == nil { t.Fatalf("不可重试的错误只应调用一次,calls=%d", calls) } } func TestRetryRespectsDeadline(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) defer cancel() start := time.Now() err := Retry(ctx, 10, 20*time.Millisecond, func(context.Context) error { return errDown }) if !errors.Is(err, context.DeadlineExceeded) || time.Since(start) > 200*time.Millisecond { t.Fatalf("应该在截止时间后停止:%v,耗时 %v", err, time.Since(start)) } } func TestBreaker(t *testing.T) { now := time.Unix(0, 0) b := NewBreaker(3, 10*time.Second) b.now = func() time.Time { return now } ctx := context.Background() fail := func(context.Context) error { return errDown } ok := func(context.Context) error { return nil } for range 3 { _ = b.Do(ctx, fail) } if err := b.Do(ctx, ok); !errors.Is(err, ErrOpen) { t.Fatalf("连续失败 3 次后应该熔断,得到 %v", err) } now = now.Add(11 * time.Second) // 冷却结束 → 半开,试探失败 → 重新打开 if err := b.Do(ctx, fail); !errors.Is(err, errDown) { t.Fatalf("半开时应放行试探请求:%v", err) } if err := b.Do(ctx, ok); !errors.Is(err, ErrOpen) { t.Fatal("试探失败后应该重新熔断") } now = now.Add(11 * time.Second) // 再次半开,试探成功 → 关闭 if err := b.Do(ctx, ok); err != nil { t.Fatal(err) } if err := b.Do(ctx, ok); err != nil { t.Fatal("恢复后应该正常放行") } // 业务错误不计入熔断 for range 5 { _ = b.Do(ctx, func(context.Context) error { return Permanent{errors.New("用户不存在")} }) } if err := b.Do(ctx, ok); err != nil { t.Fatal("业务错误不应该触发熔断") } }