fix(SSE)!: redesign EventStreamChan to prevent context pool recycling

BREAKING CHANGE: EventStreamChan signature changed from
  (chan<- Event, <-chan error)
to
  (eventChan <-chan Event)
The caller now creates and passes the channel instead of receiving it.
The errChan return value is removed.

The old non-blocking design allowed the handler to return before the SSE
stream ended, causing ServeHTTP to return the Context to the pool while
the internal goroutine was still writing to the pooled writer — a data
race across requests. The new blocking design keeps the handler inside
EventStreamChan until the event channel is closed or the client
disconnects, ensuring the Context remains bound throughout the stream.

- Caller creates channel, producer goroutine sends events
- EventStreamChan blocks handler until stream ends
- Internal goroutine captures stable references (Flusher, context.Context)
  instead of holding *Context pointer
- Nil guard on Flusher type assertion
- Add sse_test.go covering blocking, disconnect, and event format
- Update docs/sse.md for new API
This commit is contained in:
wjqserver 2026-03-29 15:42:01 +08:00
parent 8031e799d9
commit 2f94763c65
3 changed files with 188 additions and 40 deletions

View file

@ -40,27 +40,20 @@ r.GET("/events", func(c *touka.Context) {
## 模式二:通道模式 (EventStreamChan)
如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。
如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。与回调模式类似,此方法是**阻塞的**handler 会在此方法中停留,直到事件 channel 被关闭或客户端断开连接。
```go
r.GET("/events-chan", func(c *touka.Context) {
eventChan, errChan := c.EventStreamChan()
eventChan := make(chan touka.Event)
// 监听错误/断开连接
// 在独立的 goroutine 中发送事件.
go func() {
if err := <-errChan; err != nil {
log.Printf("SSE 错误: %v", err)
}
}()
// 发送数据
go func() {
defer close(eventChan) // 务必在结束时关闭
defer close(eventChan) // 务必在结束时关闭以结束事件流.
for i := 0; i < 10; i++ {
select {
case <-c.Request.Context().Done():
return
return // 客户端已断开, 退出 goroutine.
default:
eventChan <- touka.Event{
Data: fmt.Sprintf("消息 #%d", i),
@ -69,14 +62,18 @@ r.GET("/events-chan", func(c *touka.Context) {
}
}
}()
// EventStreamChan 会阻塞直到流结束.
c.EventStreamChan(eventChan)
})
```
## 最佳实践
1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。
2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。
3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx配置了足够大的写超时时间。
1. **资源回收**: `EventStreamChan` 是阻塞的handler 在事件流结束前不会返回。请确保生产者 goroutine 在 `select` 中监听 `c.Request.Context().Done()` 以响应客户端断开。
2. **关闭 Channel**: 生产者完成发送后必须 `close(eventChan)`,否则 handler 会永远阻塞。
3. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。
4. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx配置了足够大的写超时时间。
## 优雅关闭与资源清理