Repository navigation
feat(gateway): 等待上游响应头期间发送 SSE 心跳 - #473
Merged
Merged
Conversation
Owner
|
感谢,心跳的取舍写得很清楚:快失败仍走真实状态码,只有确认这次尝试慢了才提前提交 SSE 头。写锁和 pump 退出等待也看过了。 合之前还有几处:
这个分支同样落后 |
A streaming chat request goes silent between "client finished sending the body" and "upstream sent its first SSE frame". Two independent timers watch that gap and both can kill a healthy turn: an edge proxy with a fixed response budget (Cloudflare's 120s Proxy Read Timeout is not configurable below Enterprise) and the client's own idle timeout. Commit the SSE headers once the attempt proves slow (default 20s) and emit `: keepalive` comment frames every 10s until the real stream starts. Comment frames are ignored by every SSE parser, so a spec-following client cannot tell the difference. The trade-off is deliberate: committing headers forfeits the chance to answer with a real HTTP status, so the timer only fires after the deadline. Fast failures (no routable account, quota, auth) keep their proper status code; a failure after the commit degrades to an in-stream error event, which OpenAI-compatible clients already handle. Timings are tunable per deployment with CLI2API_STREAM_HEARTBEAT_AFTER_MS and CLI2API_STREAM_HEARTBEAT_INTERVAL_MS. Net/http does not make ResponseWriter safe for concurrent use, so every write goes through one mutex and Adopt/Abandon wait for the pump to exit before handing the writer back. The race detector flags any regression here, which is what caught the first draft.
caigee-cmd
force-pushed
the
feat/stream-heartbeat
branch
from
November 8, 2026 07:02
d6722aa to
f1c067b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
上游响应头迟迟不来的时候,网关在整个等待期间一个字节都不写,这段时间完全由前置代理和客户端的超时计时器支配。
问题
流式请求从「客户端发完 body」到「上游吐出第一帧 SSE」之间是一段完全静默的窗口。有两个独立的计时器在这段窗口里工作,都能掐掉一个健康的请求:
上下文越长,这个窗口越宽:长 prompt 上传本身就慢,prefill 也久,而这两段时间都发生在网关能输出任何东西之前。
做法
上游响应头超过
CLI2API_STREAM_HEARTBEAT_AFTER_MS(默认 20 秒)还没到,就先提交 SSE 响应头,然后每CLI2API_STREAM_HEARTBEAT_INTERVAL_MS(默认 10 秒)发一个: keepalive注释帧,直到真实流出开始。注释帧按 SSE 规范会被所有解析器忽略,客户端无感。取舍
提前提交响应头就放弃了用真实 HTTP 状态码回答的机会。所以计时器只在超过期限后才触发:快速失败(没有可路由账号、额度、鉴权)仍然返回原本的状态码;提交之后的失败降级为流内 error 事件,这是 OpenAI 兼容客户端本来就会处理的形式。
并发
net/http的ResponseWriter不是并发安全的。三个 goroutine 会碰它:handler 本身、心跳计时器、pump。所以所有写入走同一把锁,Adopt/Abandon会等 pump 完全退出之后才把 writer 交还给 relay。第一版没做这个等待,
-race直接报出 data race(pump 和 handler 同时写同一个ResponseWriter)。验证
stream_heartbeat_proxy_test.go里是一个对照组实验:真实Handler+ 真实ChatExecutor,中间插一个模拟固定响应预算的 TCP 代理,上游故意 5 秒不发字节,代理预算 2.5 秒。其余单元测试覆盖提交时机、快速失败保留状态码、无
Flusher时降级、pump 在接管后停止、客户端断开记录。TestHeartbeatOverRealSocketDeliversBytesBeforeBody走真实 TCP,实测首字节 134ms 到达而 body 在 700ms。go test ./internal/gateway/ -race全绿。说明
这个改动治的是「首字节慢」和「生成慢」,治不了「上传慢」——上传阶段网关还没拿到请求,心跳无从下手。那部分在另一个 PR 里用 gzip 请求体解决,两个改动互不依赖。
本 PR 由 AI 辅助生成与测试,提交前已人工审阅。