fix(wecom): improve websocket reconnection and goroutine lifecycle management

This commit is contained in:
王璐瑶 2026-03-13 09:15:20 +08:00
parent ac7e7f5a64
commit 3008a2a38c

View file

@ -503,13 +503,14 @@ func (c *WeComWSChannel) connectionManager() {
}) })
c.reconnects++ c.reconnects++
if c.reconnects > c.config.MaxReconnectAttempts { if c.config.MaxReconnectAttempts > 0 && c.reconnects > c.config.MaxReconnectAttempts {
logger.ErrorC("wecom_ws", "Max reconnection attempts reached, giving up") logger.ErrorC("wecom_ws", "Max reconnection attempts reached, but continuing to loop to ensure service alive")
// 触发错误事件 // 触发错误事件
c.eventManager.Emit(EventError, EventPayloadError{ c.eventManager.Emit(EventError, EventPayloadError{
Error: fmt.Errorf("max reconnection attempts reached"), Error: fmt.Errorf("max reconnection attempts reached"),
}) })
return // 不退出,只保留最大延迟
c.reconnects = c.config.MaxReconnectAttempts
} }
// 指数退避重连 // 指数退避重连
@ -539,14 +540,28 @@ func (c *WeComWSChannel) connectionManager() {
// 连接成功,重置重连计数 // 连接成功,重置重连计数
c.reconnects = 0 c.reconnects = 0
// 使用上下文控制这部分的读写协程生命周期
connCtx, connCancel := context.WithCancel(c.ctx)
// 启动读写协程 // 启动读写协程
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(1)
// 清空旧的发送队列,防止将上一次遗留的数据发给新建立的连接
drainLoop:
for {
select {
case <-c.sendCh:
default:
break drainLoop
}
}
// 先启动 writeLoop确保订阅消息能被发送 // 先启动 writeLoop确保订阅消息能被发送
go func() { go func() {
defer wg.Done() defer wg.Done()
c.writeLoop() defer connCancel()
c.writeLoop(connCtx)
}() }()
// 等待一小段时间确保 writeLoop 已启动 // 等待一小段时间确保 writeLoop 已启动
@ -558,17 +573,22 @@ func (c *WeComWSChannel) connectionManager() {
"error": err.Error(), "error": err.Error(),
}) })
c.closeConnection() c.closeConnection()
connCancel()
wg.Wait()
continue continue
} }
wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
c.readLoop() defer connCancel()
c.readLoop(connCtx)
}() }()
// 等待读写协程结束 // 等待读写协程结束
wg.Wait() wg.Wait()
c.closeConnection()
logger.InfoC("wecom_ws", "Connection closed, will reconnect...") logger.InfoC("wecom_ws", "Connection closed, will reconnect...")
} }
} }
@ -592,7 +612,7 @@ func (c *WeComWSChannel) connect() error {
headers := http.Header{} headers := http.Header{}
logger.DebugC("wecom_ws", "Dialing WebSocket...") logger.DebugC("wecom_ws", "Dialing WebSocket...")
conn, resp, err := dialer.Dial(c.config.WSURL, headers) conn, resp, err := dialer.DialContext(c.ctx, c.config.WSURL, headers)
if err != nil { if err != nil {
logger.ErrorCF("wecom_ws", "WebSocket dial error", map[string]any{ logger.ErrorCF("wecom_ws", "WebSocket dial error", map[string]any{
"error": err.Error(), "error": err.Error(),
@ -700,12 +720,12 @@ func (c *WeComWSChannel) closeConnection() {
} }
// readLoop 读取 WebSocket 消息 // readLoop 读取 WebSocket 消息
func (c *WeComWSChannel) readLoop() { func (c *WeComWSChannel) readLoop(ctx context.Context) {
defer c.closeConnection() defer c.closeConnection()
for { for {
select { select {
case <-c.ctx.Done(): case <-ctx.Done():
return return
default: default:
} }
@ -753,13 +773,13 @@ func (c *WeComWSChannel) readLoop() {
} }
// writeLoop 写入 WebSocket 消息 // writeLoop 写入 WebSocket 消息
func (c *WeComWSChannel) writeLoop() { func (c *WeComWSChannel) writeLoop(ctx context.Context) {
ticker := time.NewTicker(time.Duration(c.config.HeartbeatInterval) * time.Second) ticker := time.NewTicker(time.Duration(c.config.HeartbeatInterval) * time.Second)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-c.ctx.Done(): case <-ctx.Done():
return return
case msg := <-c.sendCh: case msg := <-c.sendCh:
@ -768,7 +788,8 @@ func (c *WeComWSChannel) writeLoop() {
c.connMu.RUnlock() c.connMu.RUnlock()
if conn == nil { if conn == nil {
continue logger.DebugC("wecom_ws", "Connection is nil, exiting writeLoop")
return
} }
// 更新写超时 // 更新写超时
@ -777,10 +798,20 @@ func (c *WeComWSChannel) writeLoop() {
logger.ErrorCF("wecom_ws", "WebSocket write error", map[string]any{ logger.ErrorCF("wecom_ws", "WebSocket write error", map[string]any{
"error": err.Error(), "error": err.Error(),
}) })
c.closeConnection()
return return
} }
case <-ticker.C: case <-ticker.C:
c.connMu.RLock()
conn := c.wsConn
c.connMu.RUnlock()
if conn == nil {
logger.DebugC("wecom_ws", "Connection is nil, exiting writeLoop on ticker")
return
}
// 发送心跳 // 发送心跳
if err := c.sendPing(); err != nil { if err := c.sendPing(); err != nil {
logger.ErrorCF("wecom_ws", "Ping failed", map[string]any{ logger.ErrorCF("wecom_ws", "Ping failed", map[string]any{