Skip to content

Commit 0de78f3

Browse files
committed
fix(sync): 全部 goroutine 启动点加 panic 防护(GoSafe/goSafe),防单协程异常击穿进程
1 parent dedadf3 commit 0de78f3

9 files changed

Lines changed: 55 additions & 13 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @ai-context
2+
// handlers 包内 goroutine panic 统一防护。
3+
// Why: Go 中任一 goroutine panic 且未 recover 会崩溃整个进程;WebSocket 长连接
4+
// 场景下单连接异常不应拖垮服务。本文件与 middleware/panicguard.go 分工:
5+
// middleware.GoSafe 供 main 包使用,本 goSafe 供 handlers 包内部使用(避免
6+
// handlers → middleware 反向依赖)。
7+
package handlers
8+
9+
import "log"
10+
11+
// goSafe 执行 fn 并 recover 一切 panic(记录日志,不崩溃进程)。
12+
// 用法:go goSafe(func() { ... })
13+
func goSafe(fn func()) {
14+
defer func() {
15+
if r := recover(); r != nil {
16+
log.Printf("[panicguard] goroutine recovered: %v", r)
17+
}
18+
}()
19+
fn()
20+
}

‎server/sync-service/handlers/relay.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const (
5656
)
5757

5858
func init() {
59-
go relayManager.cleanupLoop()
59+
go goSafe(relayManager.cleanupLoop)
6060
}
6161

6262
// cleanupLoop 周期回收过期配对(waiting 超 TTL / running 超最长存活)。

‎server/sync-service/handlers/rooms.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ type RoomManager struct {
7373
var roomManager = &RoomManager{rooms: make(map[string]*Room)}
7474

7575
func init() {
76-
go roomManager.cleanupLoop()
76+
go goSafe(roomManager.cleanupLoop)
7777
}
7878

7979
// 错误哨兵

‎server/sync-service/handlers/social_mirror.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ var socialMirrorManager = &SocialMirrorManager{
9090
}
9191

9292
func init() {
93-
go socialMirrorManager.cleanupLoop()
93+
go goSafe(socialMirrorManager.cleanupLoop)
9494
}
9595

9696
// ---------- SocialMirrorManager 核心方法 ----------

‎server/sync-service/handlers/websocket.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ func HandleWebSocketWithGin(c *gin.Context, userID string, deviceID string) {
170170
return
171171
}
172172

173-
go wsConn.writePump()
174-
go wsConn.readPump()
173+
go goSafe(wsConn.writePump)
174+
go goSafe(wsConn.readPump)
175175
}
176176

177177
// BroadcastOperation notifies all other online devices of a user about new operations.

‎server/sync-service/handlers/ws_connection.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ func (c *WSConnection) readPump() {
129129
// 防止单连接洪泛打满数据库连接池(上限 50)拖垮全部用户
130130
select {
131131
case c.syncReqSlots <- struct{}{}:
132-
go func() {
132+
go goSafe(func() {
133133
defer func() { <-c.syncReqSlots }()
134134
c.handleSyncRequest(payload.SinceVersion)
135-
}()
135+
})
136136
default:
137137
log.Printf("[ws] sync_request dropped (in-flight limit reached) user=%s device=%s", c.UserID, c.DeviceID)
138138
}

‎server/sync-service/handlers/ws_manager.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ func (m *WSManager) unregister(c *WSConnection) {
7171
}
7272
}
7373
// M3: 断开时从在线设备集合移除
74-
go func() {
74+
go goSafe(func() {
7575
if err := cache.SetDeviceOffline(context.Background(), c.UserID, c.DeviceID); err != nil {
7676
log.Printf("[ws] SetDeviceOffline failed user=%s device=%s: %v", c.UserID, c.DeviceID, err)
7777
}
78-
}()
78+
})
7979
}
8080

8181
// broadcastToUser sends a message to all online devices of a user, optionally excluding one device.
@@ -94,7 +94,7 @@ func (m *WSManager) broadcastToUser(userID string, excludeDeviceID string, messa
9494
if !conn.send(message) {
9595
// Buffer full or closed → close connection to protect the server.
9696
log.Printf("[ws] buffer overflow, closing user=%s device=%s", userID, deviceID)
97-
go conn.close()
97+
go goSafe(conn.close)
9898
}
9999
}
100100
}
@@ -113,7 +113,7 @@ func (m *WSManager) sendToUser(userID string, message []byte) {
113113
if !conn.send(message) {
114114
// Buffer full or closed → close connection to protect the server.
115115
log.Printf("[ws] buffer overflow, closing user=%s", userID)
116-
go conn.close()
116+
go goSafe(conn.close)
117117
}
118118
}
119119
}

‎server/sync-service/main.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,11 @@ func main() {
138138
Handler: r,
139139
}
140140

141-
go func() {
141+
go middleware.GoSafe(func() {
142142
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
143143
logger.Fatal("Failed to start server", zap.Error(err))
144144
}
145-
}()
145+
})
146146

147147
<-ctx.Done()
148148
logger.Info("shutdown signal received, closing WebSocket connections")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package middleware
2+
3+
// @ai-context
4+
// goroutine panic 统一防护:Go 中任一 goroutine panic 且未 recover 会直接崩溃整个进程。
5+
// 本包提供 GoSafe 包装器,供所有 go 启动点使用(writePump/readPump/cleanupLoop/
6+
// 异步查询等),把"单连接/单协程异常"隔离在进程之外。
7+
// Why: 2026-08 审计发现 sync-service 生产代码 0 处 defer recover(),11 处 go 启动点
8+
// 任一 panic 即整体宕机;WebSocket 长连接场景下恶意/异常帧是真实触发源。
9+
// 注意:recover 只阻止进程崩溃,不替代业务错误处理——日志必须保留上下文。
10+
11+
import "log"
12+
13+
// GoSafe 在 goroutine 内执行 fn,panic 时记录日志并吞掉异常(不崩溃进程)。
14+
// 用法:go GoSafe(func() { ... })
15+
func GoSafe(fn func()) {
16+
defer func() {
17+
if r := recover(); r != nil {
18+
log.Printf("[panicguard] goroutine recovered: %v", r)
19+
}
20+
}()
21+
fn()
22+
}

0 commit comments

Comments
 (0)