fix(relay): 修复客户端断开时误伤渠道健康评分问题

客户端断开连接(context.Canceled / DeadlineExceeded)时,原来的行为:
1. MaterializeSelection 调用 getChannelKey(ctx, ...) 因 ctx 已取消而失败
2. 失败被包装为 ErrChannelUnavailable(掩盖真实原因)
3. reportMaterializeFailure 对渠道记了一次健康分惩罚

实际上渠道本身完全正常,是客户端主动断开导致的。

修复:
- MaterializeSelection:key 查询前后各加一次 ctx.Err() 检查,
  有 ctx 错误时直接返回 ctx.Err() 而非 ErrChannelUnavailable
- relay_handler.go 调度循环:MaterializeSelection 返回 context 错误时
  静默退出(不记 channelErrors、不调 reportMaterializeFailure),
  用 context.WithoutCancel 退还预扣款项后直接返回,渠道健康分不受影响
This commit is contained in:
qian committed 2026-08-03 00:00:17 +08:00
1 parent 6683ca6d40
commit 60373a373d
2 files changed
+18

No files matched your search

+10
View File
@@ -866,6 +866,12 @@ func InitHealthScore(ctx context.Context, channelID int64) error {
// MaterializeSelection 由新调度引擎的决策构造 ChannelSelection(阶段 3):
// 转发元数据来自目录内存快照(零 DB),渠道 Key 按 keyID 解密(keyID=0 回退取首个 active Key)。
func (p *DataProviderImpl) MaterializeSelection(ctx context.Context, channelID, keyID int64, modelName string) (*common.ChannelSelection, error) {
// 优先检查上下文:客户端已断开时直接返回 ctx.Err(),而非 ErrChannelUnavailable,
// 让上层能区分"客户端断开"与"真实渠道无可用 Key",避免误伤渠道健康评分
if err := ctx.Err(); err != nil {
return nil, err
}
catalog := dispatchadapter.CatalogInstance()
if catalog == nil {
return nil, common.ErrChannelUnavailable
@@ -883,6 +889,10 @@ func (p *DataProviderImpl) MaterializeSelection(ctx context.Context, channelID,
apiKey, err = getChannelKey(ctx, channelID)
}
if err != nil || apiKey == "" {
// Key 查询失败:再次检查 ctx,区分"客户端断开导致查询中断"与"真实无可用 Key"
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, ctxErr
}
return nil, common.ErrChannelUnavailable
}
+8
View File
@@ -480,6 +480,14 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He
selection, mErr := provider.MaterializeSelection(ctx, d.Channel.ID, d.KeyID, v.lookupModel)
if mErr != nil {
// 客户端已断开(context.Canceled / DeadlineExceeded):Key 查询被中断不代表渠道有问题,
// 静默退出,不记入 channelErrors,不触发渠道健康惩罚,退还预扣款项后返回
if errors.Is(mErr, context.Canceled) || errors.Is(mErr, context.DeadlineExceeded) {
if billing != nil && preDeductAmount > 0 {
_ = billing.SettleFailed(context.WithoutCancel(ctx), rc.TenantID, rc.RequestID, preDeductAmount)
}
return nil, v.billingResult, mErr
}
// Key 解密失败 / 目录元数据缺失:按渠道级致命上报换渠道,不发起上游请求
channelErrors = append(channelErrors, fmt.Sprintf("attempt=%d channel=%d materialize_error=[%v]", attempt, d.Channel.ID, mErr))
if decision := reportMaterializeFailure(ctx, sess, mErr); decision == dispatch.DecisionAbort {