openai_compact_sse_keepalive.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. package service
  2. import (
  3. "bufio"
  4. "errors"
  5. "net"
  6. "net/http"
  7. "sync"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. )
  11. // openAICompactSSEKeepaliveKey 存放 body-signal compact 请求的下游 SSE 心跳器。
  12. const openAICompactSSEKeepaliveKey = "openai_compact_sse_keepalive"
  13. // openAICompactSSEKeepalive 在 compact 上游 unary 等待期间向下游写 SSE 注释行
  14. // 心跳。上游 /responses/compact 在模型处理期间不发送任何字节(大上下文可长达
  15. // 数分钟),下游若经过反向代理(Nginx/Cloudflare Tunnel 等),零字节静默会触发
  16. // 代理的空闲/读超时并掐断连接,Codex 只会盲目重连并重复消耗上游 compact
  17. // 配额(#3887)。SSE 注释行在 eventsource 解析层被直接忽略,不会进入客户端
  18. // 事件流。
  19. //
  20. // 首拍延迟一个 interval:绝大多数硬错误(鉴权/参数/限流)在此之前返回,仍走
  21. // 原 JSON+状态码链路(Codex 按 HTTP 状态码重试);首拍之后状态码固化为 200,
  22. // 后续错误由写回方降级为 response.failed 流内终止事件。
  23. type openAICompactSSEKeepalive struct {
  24. mu sync.Mutex
  25. writer gin.ResponseWriter
  26. started bool
  27. stopped bool
  28. // bytes 是心跳已写出的注释字节数。心跳不构成语义响应,handler 的
  29. // "Forward 期间是否已写响应"判定(failover 放弃换号的依据)必须扣除
  30. // 这部分字节,见 OpenAICompactKeepaliveAdjustedWrittenSize。
  31. bytes int
  32. stop chan struct{}
  33. }
  34. // StartOpenAICompactSSEKeepalive 为已标记 body-signal 客户端流式的 compact
  35. // 请求启动下游心跳,返回幂等的停止函数。interval<=0 或请求未标记时为 no-op。
  36. //
  37. // 同时把 c.Writer 替换为 openAICompactKeepaliveWriter:请求 goroutine 的任何
  38. // 响应构造都会先在心跳互斥锁下停拍,未被显式拦截的写回路径(如 Forward
  39. // 内部的本地拒绝)也不会与心跳 goroutine 产生数据竞争或字节交错。
  40. func StartOpenAICompactSSEKeepalive(c *gin.Context, interval time.Duration) func() {
  41. if !openAICompactClientWantsStream(c) {
  42. return func() {}
  43. }
  44. return startOpenAISSEKeepalive(c, interval)
  45. }
  46. // startOpenAISSEKeepalive 是不检查 compact 标记的内部入口,供【已经确定处于
  47. // SSE 流式上下文】的调用方使用(例如 /v1/responses 透传:进入流式循环时上游
  48. // 已返回 text/event-stream,SSE 响应头也已设好)。
  49. //
  50. // 心跳字节由 OpenAICompactKeepaliveAdjustedWrittenSize 统一排除,因此不会污染
  51. // "是否已向客户端写出语义响应"的 failover 判定(见 #3887)。
  52. func startOpenAISSEKeepalive(c *gin.Context, interval time.Duration) func() {
  53. if c == nil || c.Writer == nil || interval <= 0 {
  54. return func() {}
  55. }
  56. originalWriter := c.Writer
  57. k := &openAICompactSSEKeepalive{
  58. writer: originalWriter,
  59. stop: make(chan struct{}),
  60. }
  61. c.Set(openAICompactSSEKeepaliveKey, k)
  62. wrappedWriter := &openAICompactKeepaliveWriter{ResponseWriter: originalWriter, k: k}
  63. c.Writer = wrappedWriter
  64. var reqDone <-chan struct{}
  65. if c.Request != nil {
  66. reqDone = c.Request.Context().Done()
  67. }
  68. go func() {
  69. timer := time.NewTimer(interval)
  70. defer timer.Stop()
  71. for {
  72. select {
  73. case <-k.stop:
  74. return
  75. case <-reqDone:
  76. return
  77. case <-timer.C:
  78. }
  79. if !k.beat() {
  80. return
  81. }
  82. timer.Reset(interval)
  83. }
  84. }()
  85. return func() {
  86. k.Stop()
  87. // Do not leave a pooled middleware writer reachable through the compact
  88. // wrapper after the request finishes.
  89. if current, ok := c.Writer.(*openAICompactKeepaliveWriter); ok && current == wrappedWriter {
  90. c.Writer = originalWriter
  91. }
  92. }
  93. }
  94. // beat 在锁内提交(首次)响应头并写出一条 SSE 注释行;返回 false 表示心跳已
  95. // 停止或下游写入失败,goroutine 应退出。
  96. func (k *openAICompactSSEKeepalive) beat() bool {
  97. k.mu.Lock()
  98. defer k.mu.Unlock()
  99. if k.stopped {
  100. return false
  101. }
  102. if !k.started {
  103. header := k.writer.Header()
  104. header.Set("Content-Type", "text/event-stream")
  105. header.Set("Cache-Control", "no-cache")
  106. header.Set("Connection", "keep-alive")
  107. header.Set("X-Accel-Buffering", "no")
  108. k.writer.WriteHeader(http.StatusOK)
  109. k.started = true
  110. }
  111. n, err := k.writer.Write([]byte(": keepalive\n\n"))
  112. k.bytes += n
  113. if err != nil {
  114. k.stopped = true
  115. return false
  116. }
  117. k.writer.Flush()
  118. return true
  119. }
  120. // Stop 停止心跳;幂等,可与写回路径并发调用。
  121. func (k *openAICompactSSEKeepalive) Stop() {
  122. k.mu.Lock()
  123. k.markStoppedLocked()
  124. k.mu.Unlock()
  125. }
  126. func (k *openAICompactSSEKeepalive) markStoppedLocked() {
  127. if k.stopped {
  128. return
  129. }
  130. k.stopped = true
  131. close(k.stop)
  132. }
  133. // StopOpenAICompactSSEKeepaliveCommitted 停止当前请求的 compact 心跳(若有)
  134. // 并报告响应头是否已被心跳提交为 200。写回方以此决定继续走原 JSON/状态码
  135. // 链路,还是降级为流内终止事件。调用后不会再有心跳字节写出,且经由互斥锁
  136. // 与心跳 goroutine 建立 happens-before,调用方可安全接管 ResponseWriter。
  137. func StopOpenAICompactSSEKeepaliveCommitted(c *gin.Context) bool {
  138. if c == nil {
  139. return false
  140. }
  141. value, ok := c.Get(openAICompactSSEKeepaliveKey)
  142. if !ok {
  143. return false
  144. }
  145. k, ok := value.(*openAICompactSSEKeepalive)
  146. if !ok || k == nil {
  147. return false
  148. }
  149. k.mu.Lock()
  150. k.markStoppedLocked()
  151. committed := k.started
  152. k.mu.Unlock()
  153. return committed
  154. }
  155. // OpenAICompactKeepaliveAdjustedWrittenSize 返回排除 compact 心跳注释字节后
  156. // 的响应已写字节数;无心跳的请求等价于 c.Writer.Size()。心跳字节不构成语义
  157. // 响应——handler 以"Forward 前后 Size 是否变化"判定是否已向客户端写出响应
  158. // (变化则放弃 failover 换号),该判定不得被心跳污染,否则 compact 请求
  159. // 一旦在上游等待期间发过心跳,上游 429/5xx 就不再换号(#3887 加固审计)。
  160. // 仅心跳字节时归一化为 -1(gin 的"未写出"哨兵值),与提交前的快照可比。
  161. func OpenAICompactKeepaliveAdjustedWrittenSize(c *gin.Context) int {
  162. if c == nil || c.Writer == nil {
  163. return -1
  164. }
  165. streamKeepaliveBytes := 0
  166. if value, ok := c.Get(openAIStreamKeepaliveBytesKey); ok {
  167. streamKeepaliveBytes, _ = value.(int)
  168. }
  169. size := c.Writer.Size()
  170. compactKeepaliveBytes := 0
  171. if value, ok := c.Get(openAICompactSSEKeepaliveKey); ok {
  172. if k, valid := value.(*openAICompactSSEKeepalive); valid && k != nil {
  173. k.mu.Lock()
  174. size = k.writer.Size()
  175. compactKeepaliveBytes = k.bytes
  176. k.mu.Unlock()
  177. }
  178. }
  179. if size < 0 {
  180. return size
  181. }
  182. keepaliveBytes := compactKeepaliveBytes + streamKeepaliveBytes
  183. if keepaliveBytes <= 0 {
  184. return size
  185. }
  186. if real := size - keepaliveBytes; real > 0 {
  187. return real
  188. }
  189. return -1
  190. }
  191. // openAICompactKeepaliveWriter 包装 gin.ResponseWriter:写侧方法先停拍心跳
  192. // (互斥锁下建立 happens-before),读侧方法仅加锁不停拍——热路径的状态读取
  193. // (如 Forward 前的 Size 快照)不能误杀心跳。心跳 goroutine 直接写内层
  194. // writer(k.writer),不经过本包装器,不会递归。
  195. type openAICompactKeepaliveWriter struct {
  196. gin.ResponseWriter
  197. k *openAICompactSSEKeepalive
  198. }
  199. // suspend 停拍心跳;幂等。任何响应构造(含 Header 访问——写响应必先操作
  200. // 响应头)都视为请求侧接管 ResponseWriter。
  201. func (w *openAICompactKeepaliveWriter) suspend() {
  202. if w.k == nil {
  203. return
  204. }
  205. w.k.Stop()
  206. }
  207. func (w *openAICompactKeepaliveWriter) Header() http.Header {
  208. w.suspend()
  209. if w.ResponseWriter == nil {
  210. return http.Header{}
  211. }
  212. return w.ResponseWriter.Header()
  213. }
  214. func (w *openAICompactKeepaliveWriter) Write(data []byte) (int, error) {
  215. w.suspend()
  216. if w.ResponseWriter == nil {
  217. return 0, nil
  218. }
  219. return w.ResponseWriter.Write(data)
  220. }
  221. func (w *openAICompactKeepaliveWriter) WriteString(s string) (int, error) {
  222. w.suspend()
  223. if w.ResponseWriter == nil {
  224. return 0, nil
  225. }
  226. return w.ResponseWriter.WriteString(s)
  227. }
  228. func (w *openAICompactKeepaliveWriter) WriteHeader(code int) {
  229. w.suspend()
  230. if w.ResponseWriter == nil {
  231. return
  232. }
  233. w.ResponseWriter.WriteHeader(code)
  234. }
  235. func (w *openAICompactKeepaliveWriter) WriteHeaderNow() {
  236. w.suspend()
  237. if w.ResponseWriter == nil {
  238. return
  239. }
  240. w.ResponseWriter.WriteHeaderNow()
  241. }
  242. func (w *openAICompactKeepaliveWriter) Flush() {
  243. w.suspend()
  244. if w.ResponseWriter == nil {
  245. return
  246. }
  247. w.ResponseWriter.Flush()
  248. }
  249. func (w *openAICompactKeepaliveWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
  250. if w.ResponseWriter == nil {
  251. return nil, nil, errors.New("response writer released")
  252. }
  253. return w.ResponseWriter.Hijack()
  254. }
  255. func (w *openAICompactKeepaliveWriter) CloseNotify() <-chan bool {
  256. if w.ResponseWriter == nil {
  257. ch := make(chan bool)
  258. close(ch)
  259. return ch
  260. }
  261. return w.ResponseWriter.CloseNotify()
  262. }
  263. func (w *openAICompactKeepaliveWriter) Pusher() http.Pusher {
  264. if w.ResponseWriter == nil {
  265. return nil
  266. }
  267. return w.ResponseWriter.Pusher()
  268. }
  269. func (w *openAICompactKeepaliveWriter) Status() int {
  270. if w.k == nil || w.ResponseWriter == nil {
  271. return 0
  272. }
  273. w.k.mu.Lock()
  274. defer w.k.mu.Unlock()
  275. return w.ResponseWriter.Status()
  276. }
  277. func (w *openAICompactKeepaliveWriter) Size() int {
  278. if w.k == nil || w.ResponseWriter == nil {
  279. return 0
  280. }
  281. w.k.mu.Lock()
  282. defer w.k.mu.Unlock()
  283. return w.ResponseWriter.Size()
  284. }
  285. func (w *openAICompactKeepaliveWriter) Written() bool {
  286. if w.k == nil || w.ResponseWriter == nil {
  287. return false
  288. }
  289. w.k.mu.Lock()
  290. defer w.k.mu.Unlock()
  291. return w.ResponseWriter.Written()
  292. }