package middleware import ( "errors" "log/slog" "net/http" "time" "github.com/edu-cloud/api-gateway/internal/observability" "github.com/gin-gonic/gin" "github.com/sony/gobreaker/v2" ) // CircuitBreaker 返回针对指定服务的熔断中间件(W8 裁决:保持共享 downstream)。 // // 基于 gobreaker v2 实现: // - Interval=5s:CLOSED 状态下的统计窗口 // - ReadyToTrip:错误率 > 50% 触发 OPEN // - Timeout=30s:OPEN 持续 30 秒后转 HALF_OPEN // - MaxRequests=1:HALF_OPEN 仅允许 1 个探测请求 // // 仅当下游返回 5xx 视为失败;4xx 与 2xx 不计入熔断。 // 熔断打开或半开探测名额已满时返回 503 + ActionState 信封(W1/W2 裁决): // // {"success":false,"error":{"code":"GW_CIRCUIT_OPEN","message":"..."},"retry_after":30} func CircuitBreaker(serviceName string) gin.HandlerFunc { cb := gobreaker.NewCircuitBreaker[struct{}](gobreaker.Settings{ Name: serviceName, MaxRequests: 1, Interval: 5 * time.Second, Timeout: 30 * time.Second, ReadyToTrip: func(counts gobreaker.Counts) bool { // 请求数为 0 时不触发,避免除零 if counts.Requests == 0 { return false } // 错误率 > 50% return counts.TotalFailures*2 > counts.Requests }, OnStateChange: func(name string, from, to gobreaker.State) { observability.SetCircuitBreakerState(name, from.String(), 0) observability.SetCircuitBreakerState(name, to.String(), 1) slog.Info("circuit breaker state changed", "service", name, "from", from.String(), "to", to.String(), ) }, }) return func(c *gin.Context) { _, err := cb.Execute(func() (struct{}, error) { c.Next() // 下游 5xx 视为熔断失败 if c.Writer.Status() >= 500 { return struct{}{}, errors.New("downstream_error") } return struct{}{}, nil }) if err != nil { // 熔断打开或半开探测名额已满:返回 503 if errors.Is(err, gobreaker.ErrOpenState) || errors.Is(err, gobreaker.ErrTooManyRequests) { c.Header("Retry-After", "30") c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ "success": false, "error": gin.H{ "code": "GW_CIRCUIT_OPEN", "message": "upstream circuit breaker open", }, "retry_after": 30, }) return } // 其他情况(下游已写 5xx 响应):响应已写入,不覆盖 return } } }