Skip to content

Commit f42c8f2

Browse files
authored
Merge pull request Wei-Shaw#1062 from kunish/fix/antigravity-stream-keepalive
fix(antigravity): add stream keepalive to prevent connection drops
2 parents aa5846b + d795734 commit f42c8f2

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

backend/internal/service/antigravity_gateway_service.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,6 +3079,22 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
30793079
intervalCh = intervalTicker.C
30803080
}
30813081

3082+
// 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
3083+
keepaliveInterval := time.Duration(0)
3084+
if s.settingService.cfg != nil && s.settingService.cfg.Gateway.StreamKeepaliveInterval > 0 {
3085+
keepaliveInterval = time.Duration(s.settingService.cfg.Gateway.StreamKeepaliveInterval) * time.Second
3086+
}
3087+
var keepaliveTicker *time.Ticker
3088+
if keepaliveInterval > 0 {
3089+
keepaliveTicker = time.NewTicker(keepaliveInterval)
3090+
defer keepaliveTicker.Stop()
3091+
}
3092+
var keepaliveCh <-chan time.Time
3093+
if keepaliveTicker != nil {
3094+
keepaliveCh = keepaliveTicker.C
3095+
}
3096+
lastDataAt := time.Now()
3097+
30823098
cw := newAntigravityClientWriter(c.Writer, flusher, "antigravity gemini")
30833099

30843100
// 仅发送一次错误事件,避免多次写入导致协议混乱
@@ -3111,6 +3127,8 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
31113127
return nil, ev.err
31123128
}
31133129

3130+
lastDataAt = time.Now()
3131+
31143132
line := ev.line
31153133
trimmed := strings.TrimRight(line, "\r\n")
31163134
if strings.HasPrefix(trimmed, "data:") {
@@ -3170,6 +3188,19 @@ func (s *AntigravityGatewayService) handleGeminiStreamingResponse(c *gin.Context
31703188
logger.LegacyPrintf("service.antigravity_gateway", "Stream data interval timeout (antigravity)")
31713189
sendErrorEvent("stream_timeout")
31723190
return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
3191+
3192+
case <-keepaliveCh:
3193+
if cw.Disconnected() {
3194+
continue
3195+
}
3196+
if time.Since(lastDataAt) < keepaliveInterval {
3197+
continue
3198+
}
3199+
// SSE ping/keepalive:保持连接活跃防止 Cloudflare Tunnel 等代理断开
3200+
if !cw.Fprintf(":\n\n") {
3201+
logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity gemini), continuing to drain upstream for billing")
3202+
continue
3203+
}
31733204
}
31743205
}
31753206
}
@@ -3895,6 +3926,22 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
38953926
intervalCh = intervalTicker.C
38963927
}
38973928

3929+
// 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
3930+
keepaliveInterval := time.Duration(0)
3931+
if s.settingService.cfg != nil && s.settingService.cfg.Gateway.StreamKeepaliveInterval > 0 {
3932+
keepaliveInterval = time.Duration(s.settingService.cfg.Gateway.StreamKeepaliveInterval) * time.Second
3933+
}
3934+
var keepaliveTicker *time.Ticker
3935+
if keepaliveInterval > 0 {
3936+
keepaliveTicker = time.NewTicker(keepaliveInterval)
3937+
defer keepaliveTicker.Stop()
3938+
}
3939+
var keepaliveCh <-chan time.Time
3940+
if keepaliveTicker != nil {
3941+
keepaliveCh = keepaliveTicker.C
3942+
}
3943+
lastDataAt := time.Now()
3944+
38983945
cw := newAntigravityClientWriter(c.Writer, flusher, "antigravity claude")
38993946

39003947
// 仅发送一次错误事件,避免多次写入导致协议混乱
@@ -3947,6 +3994,8 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
39473994
return nil, fmt.Errorf("stream read error: %w", ev.err)
39483995
}
39493996

3997+
lastDataAt = time.Now()
3998+
39503999
// 处理 SSE 行,转换为 Claude 格式
39514000
claudeEvents := processor.ProcessLine(strings.TrimRight(ev.line, "\r\n"))
39524001
if len(claudeEvents) > 0 {
@@ -3969,6 +4018,20 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
39694018
logger.LegacyPrintf("service.antigravity_gateway", "Stream data interval timeout (antigravity)")
39704019
sendErrorEvent("stream_timeout")
39714020
return &antigravityStreamResult{usage: convertUsage(nil), firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
4021+
4022+
case <-keepaliveCh:
4023+
if cw.Disconnected() {
4024+
continue
4025+
}
4026+
if time.Since(lastDataAt) < keepaliveInterval {
4027+
continue
4028+
}
4029+
// SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4030+
// 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4031+
if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") {
4032+
logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity claude), continuing to drain upstream for billing")
4033+
continue
4034+
}
39724035
}
39734036
}
39744037
}
@@ -4299,6 +4362,22 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
42994362
intervalCh = intervalTicker.C
43004363
}
43014364

4365+
// 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
4366+
keepaliveInterval := time.Duration(0)
4367+
if s.settingService.cfg != nil && s.settingService.cfg.Gateway.StreamKeepaliveInterval > 0 {
4368+
keepaliveInterval = time.Duration(s.settingService.cfg.Gateway.StreamKeepaliveInterval) * time.Second
4369+
}
4370+
var keepaliveTicker *time.Ticker
4371+
if keepaliveInterval > 0 {
4372+
keepaliveTicker = time.NewTicker(keepaliveInterval)
4373+
defer keepaliveTicker.Stop()
4374+
}
4375+
var keepaliveCh <-chan time.Time
4376+
if keepaliveTicker != nil {
4377+
keepaliveCh = keepaliveTicker.C
4378+
}
4379+
lastDataAt := time.Now()
4380+
43024381
flusher, _ := c.Writer.(http.Flusher)
43034382
cw := newAntigravityClientWriter(c.Writer, flusher, "antigravity upstream")
43044383

@@ -4316,6 +4395,8 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
43164395
return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs}
43174396
}
43184397

4398+
lastDataAt = time.Now()
4399+
43194400
line := ev.line
43204401

43214402
// 记录首 token 时间
@@ -4341,6 +4422,20 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
43414422
}
43424423
logger.LegacyPrintf("service.antigravity_gateway", "Stream data interval timeout (antigravity upstream)")
43434424
return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs}
4425+
4426+
case <-keepaliveCh:
4427+
if cw.Disconnected() {
4428+
continue
4429+
}
4430+
if time.Since(lastDataAt) < keepaliveInterval {
4431+
continue
4432+
}
4433+
// SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4434+
// 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4435+
if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") {
4436+
logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity upstream), continuing to drain upstream for billing")
4437+
continue
4438+
}
43444439
}
43454440
}
43464441
}

0 commit comments

Comments
 (0)