From 1066a9b1cf1806105e07c840d15d4cc2ae2aef15 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:34:00 +0000 Subject: [PATCH 1/2] docs: add detailed Chinese documentation and SSE graceful shutdown example Added a comprehensive \`docs\` folder and updated README.md. Included a specific example in \`docs/sse.md\` demonstrating how to handle \`c.Request.Context().Done()\` for graceful SSE connection closure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- docs/sse.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/sse.md b/docs/sse.md index 7927f25..5a75731 100644 --- a/docs/sse.md +++ b/docs/sse.md @@ -77,3 +77,44 @@ r.GET("/events-chan", func(c *touka.Context) { 1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。 2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。 3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。 + +## 优雅关闭与资源清理 + +在长连接场景下,正确处理客户端断开或服务器关闭信号至关重要,以防止资源泄漏。 + +### 示例:监听 Context 取消信号 + +```go +r.GET("/events-graceful", func(c *touka.Context) { + // 设置响应头(如果手动处理,EventStream 会自动设置) + + // 使用 Context 的 Done 通道来感知连接关闭 + ctx := c.Request.Context() + + // 启动一个用于模拟数据生成的循环 + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + c.EventStream(func(w io.Writer) bool { + select { + case <-ctx.Done(): + // 收到优雅关闭信号(客户端离开或服务器正在关闭) + fmt.Println("SSE 连接正在关闭,开始清理资源...") + return false // 返回 false 告知框架停止流 + + case t := <-ticker.C: + event := touka.Event{ + Data: "Tick at " + t.Format(time.RFC3339), + } + if err := event.Render(w); err != nil { + return false + } + return true + } + }) + + fmt.Println("SSE 连接已彻底释放") +}) +``` + +在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。 From 2e55db4e2ddf03aee53cb8f95dfcca6890fb9ba1 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:52:42 +0000 Subject: [PATCH 2/2] docs: add comprehensive documentation and fix SSE graceful shutdown - Created a detailed \`docs\` directory with Chinese documentation for all major features. - Implemented \`BaseContext\` propagation in \`Engine\` to allow long-lived connections (like SSE) to receive shutdown signals. - Added \`Engine.Context()\` to provide access to the shutdown context. - Updated \`docs/sse.md\` with a guide on handling graceful shutdowns. - Cleaned up temporary build/update scripts. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- docs/sse.md | 11 +++++++++++ engine.go | 10 ++++++++++ serve.go | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/docs/sse.md b/docs/sse.md index 5a75731..bafc553 100644 --- a/docs/sse.md +++ b/docs/sse.md @@ -118,3 +118,14 @@ r.GET("/events-graceful", func(c *touka.Context) { ``` 在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。 + +### 为什么会出现 "context deadline exceeded"? + +如果您在优雅停机时遇到 `context deadline exceeded` 错误,通常是因为 SSE 连接仍然活跃,而 `http.Server.Shutdown` 正在等待它们结束。 + +在 Touka 的新版本中,我们通过 `BaseContext` 将 `Engine` 的关闭信号注入到了每个请求的 `Context` 中。这意味着: +1. 当服务器收到关闭信号时,`engine.shutdownCtx` 会被取消。 +2. 随后,所有活跃请求的 `c.Request.Context()` 也会收到取消信号。 +3. 您的 SSE 处理器中的 `case <-c.Request.Context().Done():` 会立即触发,从而优雅地结束连接。 + +**注意:** 请务必使用 `RunShutdown`、`RunTLS` 或 `RunTLSRedir` 来启动服务器,以便框架能自动管理这些信号。 diff --git a/engine.go b/engine.go index 0a95765..1e7bb18 100644 --- a/engine.go +++ b/engine.go @@ -67,6 +67,9 @@ type Engine struct { Protocols ProtocolsConfig //协议版本配置 useDefaultProtocols bool //是否使用默认协议 + shutdownCtx context.Context + shutdownCancel context.CancelFunc + // ServerConfigurator 允许在服务器启动前对其进行自定义配置 // 例如,设置 ReadTimeout, WriteTimeout 等 ServerConfigurator func(*http.Server) @@ -207,6 +210,7 @@ func New() *Engine { TLSServerConfigurator: nil, GlobalMaxRequestBodySize: -1, } + engine.shutdownCtx, engine.shutdownCancel = context.WithCancel(context.Background()) //engine.SetProtocols(GetDefaultProtocolsConfig()) engine.SetDefaultProtocols() engine.SetLoggerCfg(defaultLogRecoConfig) @@ -766,3 +770,9 @@ func (engine *Engine) handleRequest(c *Context) { c.Next() // 执行处理函数链 //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 } + +// Context 返回 Engine 的根上下文, 该上下文在服务器优雅关闭时会被取消. +// 它可以用于在长连接 (如 SSE) 中监听关闭信号. +func (engine *Engine) Context() context.Context { + return engine.shutdownCtx +} diff --git a/serve.go b/serve.go index 7e05b8c..6a4cf2a 100644 --- a/serve.go +++ b/serve.go @@ -224,7 +224,11 @@ func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error srv := &http.Server{ Addr: addr, Handler: engine, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 //engine.applyDefaultServerConfig(srv) @@ -241,7 +245,11 @@ func (engine *Engine) RunShutdownWithContext(addr string, ctx context.Context, t srv := &http.Server{ Addr: addr, Handler: engine, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 //engine.applyDefaultServerConfig(srv) @@ -270,7 +278,11 @@ func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...tim Addr: addr, Handler: engine, TLSConfig: tlsConfig, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + srv.RegisterOnShutdown(engine.shutdownCancel) // 应用框架的默认配置和用户提供的自定义配置 // 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator @@ -304,7 +316,11 @@ func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Con Addr: httpsAddr, Handler: engine, TLSConfig: tlsConfig, + BaseContext: func(l net.Listener) context.Context { + return engine.shutdownCtx + }, } + httpsSrv.RegisterOnShutdown(engine.shutdownCancel) //engine.applyDefaultServerConfig(httpsSrv) if engine.TLSServerConfigurator != nil { engine.TLSServerConfigurator(httpsSrv)