Skip to content

Commit 3741617

Browse files
author
QTom
committed
fix(gateway): WS 连接池条件式 MarkBroken 防止跨请求串流
正常终端事件(response.completed 等)退出后连接归还复用, 仅异常路径(读写错误、error 事件、客户端断连)MarkBroken 销毁。 Generate 模式: - 引入 cleanExit 标记,仅在 isTerminalEvent break 时设置 true - defer 中根据 cleanExit 决定是否 MarkBroken - 所有异常路径已在各自分支中提前调用 MarkBroken Ingress 模式: - 引入 lastTurnClean 标记,sendAndRelay 正常完成时设为 true - releaseSessionLease 根据 lastTurnClean 决定是否 MarkBroken - 错误路径重置 lastTurnClean = false - 客户端断连后 drain 仍保守 MarkBroken(L2916)
1 parent ab4e8b2 commit 3741617

2 files changed

Lines changed: 25 additions & 5 deletions

File tree

backend/internal/service/openai_ws_forwarder.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,7 +1870,16 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2(
18701870
}
18711871
return nil, wrapOpenAIWSFallback(classifyOpenAIWSAcquireError(err), err)
18721872
}
1873-
defer lease.Release()
1873+
// cleanExit 标记正常终端事件退出,此时上游不会再发送帧,连接可安全归还复用。
1874+
// 所有异常路径(读写错误、error 事件等)已在各自分支中提前调用 MarkBroken,
1875+
// 因此 defer 中只需处理正常退出时不 MarkBroken 即可。
1876+
cleanExit := false
1877+
defer func() {
1878+
if !cleanExit {
1879+
lease.MarkBroken()
1880+
}
1881+
lease.Release()
1882+
}()
18741883
connID := strings.TrimSpace(lease.ConnID())
18751884
logOpenAIWSModeDebug(
18761885
"connected account_id=%d account_type=%s transport=%s conn_id=%s conn_reused=%v conn_pick_ms=%d queue_wait_ms=%d has_previous_response_id=%v",
@@ -2248,6 +2257,7 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2(
22482257
}
22492258

22502259
if isTerminalEvent {
2260+
cleanExit = true
22512261
break
22522262
}
22532263
}
@@ -2983,12 +2993,15 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
29832993
pinnedSessionConnID = connID
29842994
}
29852995
}
2996+
// lastTurnClean 标记最后一轮 sendAndRelay 是否正常完成(收到终端事件且客户端未断连)。
2997+
// 所有异常路径(读写错误、error 事件、客户端断连)已在各自分支或上层(L3403)中 MarkBroken,
2998+
// 因此 releaseSessionLease 中只需在非正常结束时 MarkBroken。
2999+
lastTurnClean := false
29863000
releaseSessionLease := func() {
29873001
if sessionLease == nil {
29883002
return
29893003
}
2990-
if dedicatedMode {
2991-
// dedicated 会话结束后主动标记损坏,确保连接不会跨会话复用。
3004+
if !lastTurnClean {
29923005
sessionLease.MarkBroken()
29933006
}
29943007
unpinSessionConn(sessionConnID)
@@ -3383,6 +3396,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
33833396

33843397
result, relayErr := sendAndRelay(turn, sessionLease, currentPayload, currentPayloadBytes, currentOriginalModel)
33853398
if relayErr != nil {
3399+
lastTurnClean = false
33863400
if recoverIngressPrevResponseNotFound(relayErr, turn, connID) {
33873401
continue
33883402
}
@@ -3402,6 +3416,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
34023416
turnRetry = 0
34033417
turnPrevRecoveryTried = false
34043418
lastTurnFinishedAt = time.Now()
3419+
lastTurnClean = true
34053420
if hooks != nil && hooks.AfterTurn != nil {
34063421
hooks.AfterTurn(turn, result, nil)
34073422
}

backend/internal/service/openai_ws_forwarder_success_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,8 @@ func TestOpenAIGatewayService_Forward_WSv2_PoolReuseNotOneToOne(t *testing.T) {
380380
require.True(t, strings.HasPrefix(result.RequestID, "resp_reuse_"))
381381
}
382382

383-
require.Equal(t, int64(1), upgradeCount.Load(), "多个客户端请求应复用账号连接池而不是 1:1 对等建链")
383+
// 条件式 MarkBroken:正常终端事件退出后连接归还复用,不再无条件销毁。
384+
require.Equal(t, int64(1), upgradeCount.Load(), "正常完成后连接应归还复用,不应每次新建")
384385
metrics := svc.SnapshotOpenAIWSPoolMetrics()
385386
require.GreaterOrEqual(t, metrics.AcquireReuseTotal, int64(1))
386387
require.GreaterOrEqual(t, metrics.ConnPickTotal, int64(1))
@@ -964,6 +965,10 @@ func TestOpenAIGatewayService_Forward_WSv2_TurnMetadataInPayloadOnConnReuse(t *t
964965
require.NotNil(t, result1)
965966
require.Equal(t, "resp_meta_1", result1.RequestID)
966967

968+
require.Len(t, captureConn.writes, 1)
969+
firstWrite := requestToJSONString(captureConn.writes[0])
970+
require.Equal(t, "turn_meta_payload_1", gjson.Get(firstWrite, "client_metadata.x-codex-turn-metadata").String())
971+
967972
rec2 := httptest.NewRecorder()
968973
c2, _ := gin.CreateTestContext(rec2)
969974
c2.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
@@ -977,7 +982,7 @@ func TestOpenAIGatewayService_Forward_WSv2_TurnMetadataInPayloadOnConnReuse(t *t
977982
require.Equal(t, 1, captureDialer.DialCount(), "同一账号两轮请求应复用同一 WS 连接")
978983
require.Len(t, captureConn.writes, 2)
979984

980-
firstWrite := requestToJSONString(captureConn.writes[0])
985+
firstWrite = requestToJSONString(captureConn.writes[0])
981986
secondWrite := requestToJSONString(captureConn.writes[1])
982987
require.Equal(t, "turn_meta_payload_1", gjson.Get(firstWrite, "client_metadata.x-codex-turn-metadata").String())
983988
require.Equal(t, "turn_meta_payload_2", gjson.Get(secondWrite, "client_metadata.x-codex-turn-metadata").String())

0 commit comments

Comments
 (0)