依据 coord-final-decisions §3.8 W1-W8 裁决与 president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化: - W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体 - W3: 全量替换为 log/slog 结构化日志 - W4: /readyz 并行 ping 9 下游 + 软失败规则 - W5: 7 个业务 Prometheus 指标 + /metrics 端点 - W6: tracer 资源属性补全(name/version/env/host) - W7: DevMode=true && ENV=production panic 防护 - W8: 保持共享 downstream 熔断 P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。 P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。 文档同步:README/01/02/known-issues,arch.db 已更新。 质量校验:go vet + build + test 均通过。
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
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
|
||
}
|
||
}
|
||
}
|