Files
cpdaily_xiaozhushou/backed/internal/handler/leave_stream_ctx_test.go
T
becomer 00bd63cfdf 修复销假/请假 SSE 崩溃:stream writer 内误用已回收的 fasthttp RequestCtx
线上 v1.2.158 销假必崩,前端显示「连接失败,请检查网络」,用户误判为网络问题。

根因(162 服务器日志实证):
SetBodyStreamWriter 的回调运行在 fasthttp 的独立写协程中,此时 *RequestCtx
已被归还复用。回调内调用 newXsgzSession(c.Context(), ...) 把该 ctx 交给
resty 的 SetContext,resty 内部走 context.WithDeadline → propagateCancel
→ parent.Done(),而 RequestCtx.Done() 直接解引用 ctx.s(server.go:2726):
    panic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV]  fasthttp.(*RequestCtx).Done  server.go:2726
此时 s 已为 nil,触发 SIGSEGV。

panic 发生在协程里,fiber 的 recover 中间件捕获不到,直接杀死整个进程,
前端 XHR 连接被掐断 → 只剩「连接失败,请检查网络」这条误导性提示。

修复:
1. 新增 streamCtx():返回 context.WithTimeout(Background, 3min),
   脱离请求生命周期,回调内一律使用它(销假与请假两条链路);
2. 两个回调都加 recover 兜底,即便再出现协程内 panic 也只返回
   task_error 事件,不会拖垮进程。

回归测试:TestStreamCtxIsUsable 保证 ctx 具备 Done/deadline;
TestRequestCtxDonePanicsAfterReset 用零值 RequestCtx 固化崩溃机制,
解释为什么不能用 c.Context()。
2026-10-17 18:07:34 +08:00

77 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"testing"
"time"
"github.com/valyala/fasthttp"
)
// 回归测试:SSE stream writer 回调中不能使用 fasthttp 的 RequestCtx。
//
// 线上事故(v1.2.158):销假接口在 stream writer 里调用
// newXsgzSession(c.Context(), ...),而该回调运行在 fasthttp 的独立写协程中,
// 此时 RequestCtx 已被 reset(ctx.s == nil)。resty 的 SetContext 会走
// context.WithDeadline → propagateCancel → parent.Done(),
// 而 RequestCtx.Done() 解引用 ctx.s 直接 SIGSEGV:
//
// panic: runtime error: invalid memory address or nil pointer dereference
// [signal SIGSEGV: ... addr=0x560]
// fasthttp.(*RequestCtx).Done(...) server.go:2726
//
// panic 发生在协程里,fiber 的 recover 中间件拦不住,整个进程退出,
// 前端 SSE 连接被掐断,只能显示「连接失败,请检查网络」。
// TestStreamCtxIsUsable 保证 streamCtx 返回的 context 是完整可用的。
func TestStreamCtxIsUsable(t *testing.T) {
ctx, cancel := streamCtx()
defer cancel()
if ctx == nil {
t.Fatal("streamCtx 不应返回 nil")
}
// 必须支持 Done():resty 在 SetContext 后依赖它注册取消传播
done := ctx.Done()
if done == nil {
t.Fatal("Done() 不应为 nil,否则 resty 内部 propagateCancel 会出错")
}
// 必须有 deadline:避免上游某一跳卡住导致协程永不释放
dl, ok := ctx.Deadline()
if !ok {
t.Fatal("streamCtx 应带 deadline")
}
if time.Until(dl) <= 0 {
t.Fatal("deadline 应在未来")
}
// 取 Done 不应 panic
select {
case <-done:
t.Fatal("新建的 context 不应已取消")
default:
}
}
// TestRequestCtxDonePanicsAfterReset 直接复现线上崩溃的根因。
//
// 零值的 fasthttp.RequestCtx 即 ctx.s == nil,等价于 stream writer 回调中
// 被 reset 之后的状态。调用其 Done() 会解引用 nil 的 ctx.s 而 SIGSEGV
// (server.go:2726: func (ctx *RequestCtx) Done() { return ctx.s.done })。
//
// 该用例存在的意义:把「为什么不能在回调里用 c.Context()」固化下来。
// 若将来 fasthttp 改成安全返回 nil,此用例会 skip,届时可考虑移除 streamCtx。
func TestRequestCtxDonePanicsAfterReset(t *testing.T) {
var rctx fasthttp.RequestCtx // 零值:内部 s 为 nil
defer func() {
if r := recover(); r == nil {
t.Skip("fasthttp 已修复:RequestCtx.Done 在 s==nil 时不再 panic")
}
}()
_ = rctx.Done() // 线上就是这样崩的
t.Fatal("预期 panic 未发生")
}