// Package resilience 服务调用的三件套:超时(由调用方的 context 控制)、带退避的重试、熔断器。只用标准库。 package resilience import ( "context" "errors" "math/rand/v2" "sync" "time" ) // ErrOpen 熔断器打开时直接返回,不再调用下游 var ErrOpen = errors.New("熔断中:下游暂时不可用") // Permanent 包装不应该重试的错误(参数错误、没有权限、资源不存在等) type Permanent struct{ Err error } func (p Permanent) Error() string { return p.Err.Error() } func (p Permanent) Unwrap() error { return p.Err } // Retry 最多尝试 attempts 次,间隔指数增长并加随机抖动;context 取消或超时立即停止 func Retry(ctx context.Context, attempts int, base time.Duration, fn func(context.Context) error) error { var err error for i := range attempts { if err = fn(ctx); err == nil { return nil } var p Permanent if errors.As(err, &p) || errors.Is(err, ErrOpen) || i == attempts-1 { return err } backoff := base << i // base, 2*base, 4*base ... backoff += time.Duration(rand.Int64N(int64(backoff)/2 + 1)) // 抖动,避免所有客户端同时重试 select { case <-ctx.Done(): return errors.Join(err, ctx.Err()) case <-time.After(backoff): } } return err } type state int const ( closed state = iota // 正常放行 open // 熔断:直接拒绝 halfOpen // 试探:放一个请求过去 ) // Breaker 连续失败 threshold 次后打开,cooldown 之后进入半开,试探成功则关闭、失败则重新打开 type Breaker struct { mu sync.Mutex state state failures int threshold int cooldown time.Duration openedAt time.Time probing bool now func() time.Time // 测试时可以替换 } func NewBreaker(threshold int, cooldown time.Duration) *Breaker { return &Breaker{threshold: threshold, cooldown: cooldown, now: time.Now} } func (b *Breaker) allow() bool { b.mu.Lock() defer b.mu.Unlock() switch b.state { case open: if b.now().Sub(b.openedAt) < b.cooldown { return false } b.state = halfOpen fallthrough case halfOpen: if b.probing { return false // 半开状态只放一个试探请求 } b.probing = true return true } return true } func (b *Breaker) record(err error) { b.mu.Lock() defer b.mu.Unlock() var p Permanent failed := err != nil && !errors.As(err, &p) // 业务错误不算下游故障 if b.state == halfOpen { b.probing = false if failed { b.state, b.openedAt = open, b.now() } else { b.state, b.failures = closed, 0 } return } if !failed { b.failures = 0 return } b.failures++ if b.failures >= b.threshold { b.state, b.openedAt = open, b.now() } } // Do 通过熔断器调用 fn func (b *Breaker) Do(ctx context.Context, fn func(context.Context) error) error { if !b.allow() { return ErrOpen } err := fn(ctx) b.record(err) return err }