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