Files

83 lines
2.3 KiB
Go

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("业务错误不应该触发熔断")
}
}