Skip to content

支持New优雅关闭,避免重启时流式回复被打断#4258

Open
feitianbubu wants to merge 1 commit intoQuantumNous:mainfrom
feitianbubu:pr/8486b0d77893aecd72790f949022eff581262042
Open

支持New优雅关闭,避免重启时流式回复被打断#4258
feitianbubu wants to merge 1 commit intoQuantumNous:mainfrom
feitianbubu:pr/8486b0d77893aecd72790f949022eff581262042

Conversation

@feitianbubu
Copy link
Copy Markdown
Contributor

@feitianbubu feitianbubu commented Apr 15, 2026

⚠️ 提交说明 / PR Notice

支持服务优雅关闭,避免重启时流式回复被打断

📝 变更描述 / Description

将 main.go 中原来的 server.Run() 阻塞启动方式改为 http.Server + goroutine 启动,监听 SIGINT/SIGTERM 信号后调用 srv.Shutdown() 实现优雅关闭。
这样在部署重启或容器停止时,已有的请求(特别是 SSE 长连接)能在超时时间内正常完成,而不是被直接杀掉。超时时间通过环境变量 SHUTDOWN_TIMEOUT_SECONDS 控制,默认 120 秒。

📸 运行证明 / Proof of Work

  1. docker compose启动
  2. 执行一个较长的流式回复
  3. docker compose up -d
  4. 系统会等待流式回复结束再重启, 最长等待120s(可配置)
image image

Summary by CodeRabbit

  • Refactor
    • Improved server startup and graceful shutdown mechanism with proper termination signal handling (default 120-second timeout). The server now cleanly exits when receiving shutdown signals instead of abrupt termination.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

Walkthrough

Replaced the blocking server.Run() call with an explicit http.Server instance running in a goroutine. Added graceful shutdown handling: the server now listens for SIGINT/SIGTERM signals and initiates a controlled shutdown with a configurable timeout (default 120 seconds) before process termination.

Changes

Cohort / File(s) Summary
Server Initialization & Graceful Shutdown
main.go
Refactored server startup from blocking call to explicit http.Server with goroutine-based operation. Introduced signal handling (SIGINT/SIGTERM), graceful shutdown with timeout context (SHUTDOWN_TIMEOUT_SECONDS), and improved error handling for shutdown scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A server that runs wild and free,
Now halts with grace when signals decree,
No abrupt exits—just a polite goodbye,
With timeout buffers as threads unwind, oh my!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title in Chinese describes graceful shutdown to prevent streaming responses from being interrupted during restarts, which directly matches the main code change implementing graceful shutdown with signal handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.go`:
- Around line 215-222: The HTTP server shutdown only stops srv; you must also
wait for the relay/background goroutine pool (common.relayGoPool from
common/gopool.go) to drain before exiting main. After calling srv.Shutdown(ctx)
and before returning, call the relay pool's shutdown/wait method (e.g.,
common.relayGoPool.Shutdown(ctx) or common.relayGoPool.Wait()) or implement a
drain loop that checks relayGoPool.ActiveWorkers()/len(relayGoPool.workers) and
blocks until zero (respecting the same ctx timeout) so long-lived relay tasks
complete gracefully.
- Around line 196-207: Move the LogStartupSuccess call to after you successfully
bind the socket: perform a net.Listen("tcp", ":"+port) first (add the net
import), if listen returns without error then call
common.LogStartupSuccess(startTime, port), then start the server with
srv.Serve(listener) in the goroutine (or wrap listener in the existing srv).
Update the goroutine to use srv.Serve(listener) and still treat
http.ErrServerClosed as non-fatal; ensure the listener is closed on shutdown so
resources are cleaned up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e26b240-b77a-4b94-a32c-1f08b5e4c444

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8661d and f053843.

📒 Files selected for processing (1)
  • main.go

Comment thread main.go
Comment on lines 196 to +207
// Log startup success message
common.LogStartupSuccess(startTime, port)

err = server.Run(":" + port)
if err != nil {
common.FatalLog("failed to start HTTP server: " + err.Error())
srv := &http.Server{
Addr: ":" + port,
Handler: server,
}

go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
common.FatalLog("failed to start HTTP server: " + err.Error())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bind the listener before logging startup success.

Line 197 logs a successful startup before Line 205 has actually bound the port. If the bind fails, operators still get a false-positive “started” log and the real failure only appears from a background goroutine.

Suggested shape
- common.LogStartupSuccess(startTime, port)
-
- srv := &http.Server{
- 	Addr:    ":" + port,
- 	Handler: server,
- }
-
- go func() {
- 	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
- 		common.FatalLog("failed to start HTTP server: " + err.Error())
- 	}
- }()
+ ln, err := net.Listen("tcp", ":"+port)
+ if err != nil {
+ 	common.FatalLog("failed to start HTTP server: " + err.Error())
+ }
+ common.LogStartupSuccess(startTime, port)
+
+ srv := &http.Server{
+ 	Handler: server,
+ }
+
+ go func() {
+ 	if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ 		common.FatalLog("failed to start HTTP server: " + err.Error())
+ 	}
+ }()

Also add the net import.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @main.go around lines 196 - 207, Move the LogStartupSuccess call to after you
successfully bind the socket: perform a net.Listen("tcp", ":"+port) first (add
the net import), if listen returns without error then call
common.LogStartupSuccess(startTime, port), then start the server with
srv.Serve(listener) in the goroutine (or wrap listener in the existing srv).
Update the goroutine to use srv.Serve(listener) and still treat
http.ErrServerClosed as non-fatal; ensure the listener is closed on shutdown so
resources are cleaned up.


</details>

<!-- fingerprinting:phantom:medusa:grasshopper:322129d1-9e03-4e22-80a1-b6f30e657d48 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread main.go
Comment on lines +215 to +222
// SSE streams may run for minutes; give them time to finish before forced exit
shutdownTimeout := time.Duration(common.GetEnvOrDefault("SHUTDOWN_TIMEOUT_SECONDS", 120)) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
common.SysError(fmt.Sprintf("server forced to shutdown: %v", err))
}
common.SysLog("server exited")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wait for relay/background workers before returning from main.

srv.Shutdown(ctx) only waits for the http.Server. The relay path also uses common.relayGoPool (common/gopool.go:1-23), and this file starts several long-lived goroutines above. Line 222 returns from main immediately afterward, so anything still running outside the HTTP server is still terminated abruptly on process exit. That leaves a gap in the graceful-shutdown guarantee for streams offloaded to those workers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.go` around lines 215 - 222, The HTTP server shutdown only stops srv; you
must also wait for the relay/background goroutine pool (common.relayGoPool from
common/gopool.go) to drain before exiting main. After calling srv.Shutdown(ctx)
and before returning, call the relay pool's shutdown/wait method (e.g.,
common.relayGoPool.Shutdown(ctx) or common.relayGoPool.Wait()) or implement a
drain loop that checks relayGoPool.ActiveWorkers()/len(relayGoPool.workers) and
blocks until zero (respecting the same ctx timeout) so long-lived relay tasks
complete gracefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant