Files

672 lines
17 KiB
Go
Raw Permalink 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 websocket
import (
"context"
"encoding/json"
"errors"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/pika-monitor/pika/internal/protocol"
"go.uber.org/zap"
)
const (
serverPingInterval = 10 * time.Second
serverPongWait = 30 * time.Second
serverWriteWait = 10 * time.Second
// 可靠事件必须保持会话内顺序;队列满表示服务端已无法及时消费,
// 断开当前传输后由 agent 保留 outbox 并重连重放。
reliableQueueSize = 512
reliableQueueWait = 2 * time.Second
// 普通遥测是 latest-wins,不应阻塞可靠事件。队列满时丢弃本批,
// agent 下一轮会继续上报更新后的快照。
bestEffortQueueSize = 128
serverSendWait = 3 * time.Second
messageRetryMin = 500 * time.Millisecond
messageRetryMax = 10 * time.Second
)
// MessageHandler 处理一条 agent 上行消息。
type MessageHandler func(ctx context.Context, agentID string, messageType string, data json.RawMessage) error
// PongHandler 处理连接 pong。
type PongHandler func(client *Client)
// PermanentMessageError 表示重放也无法成功的坏消息,例如 JSON 结构错误。
// 可靠处理器会记录并确认这类消息,避免单条毒消息永久阻塞后续事件。
type PermanentMessageError struct {
err error
}
func (e *PermanentMessageError) Error() string { return e.err.Error() }
func (e *PermanentMessageError) Unwrap() error { return e.err }
// Permanent 把不可重试的消息错误标记为永久错误。
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentMessageError{err: err}
}
func isPermanent(err error) bool {
var target *PermanentMessageError
return errors.As(err, &target)
}
type agentMessage struct {
seq uint64
typ string
data json.RawMessage
}
// agentSession 是可靠投递状态的唯一所有者。它绑定 agentID + BootID,
// 与具体 WebSocket 连接解耦:同一进程重连复用 session,新进程注册时
// 原子替换 session,旧 worker 即使仍在返回路径上也无法提交 ACK。
type agentSession struct {
agentID string
bootID string
ctx context.Context
cancel context.CancelFunc
reliableQueue chan agentMessage
bestEffortQueue chan agentMessage
ackSeq atomic.Uint64
}
func newAgentSession(agentID, bootID string) *agentSession {
ctx, cancel := context.WithCancel(context.Background())
return &agentSession{
agentID: agentID,
bootID: bootID,
ctx: ctx,
cancel: cancel,
reliableQueue: make(chan agentMessage, reliableQueueSize),
bestEffortQueue: make(chan agentMessage, bestEffortQueueSize),
}
}
// Client 只代表一次 WebSocket 传输,不拥有可靠会话状态。
type Client struct {
ID string
conn *websocket.Conn
manager *Manager
session *agentSession
send chan []byte
done chan struct{}
closeOnce sync.Once
lastActive atomic.Int64
lastStatusWrite atomic.Int64
}
// NewClient 创建尚未注册到 Manager 的连接。
func NewClient(agentID string, conn *websocket.Conn, manager *Manager) *Client {
c := &Client{
ID: agentID,
conn: conn,
manager: manager,
send: make(chan []byte, 512),
done: make(chan struct{}),
}
c.touch()
return c
}
func (c *Client) touch() {
c.lastActive.Store(time.Now().UnixMilli())
}
func (c *Client) inactiveFor(now time.Time) time.Duration {
last := c.lastActive.Load()
if last == 0 {
return 0
}
return now.Sub(time.UnixMilli(last))
}
func (c *Client) close() {
c.closeOnce.Do(func() {
close(c.done)
if c.conn != nil {
_ = c.conn.Close()
}
})
}
// Close 主动关闭本次传输;Handler 的退出路径负责同步注销 current client。
func (c *Client) Close() {
c.close()
}
func (c *Client) sendWithTimeout(message []byte, timeout time.Duration) error {
select {
case <-c.done:
return ErrClientNotFound
default:
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-c.done:
return ErrClientNotFound
case c.send <- message:
return nil
case <-timer.C:
return ErrSendTimeout
}
}
func (c *Client) trySend(message []byte) bool {
select {
case <-c.done:
return false
default:
}
select {
case <-c.done:
return false
case c.send <- message:
return true
default:
return false
}
}
// Manager 同步维护当前连接和每个 agent 的会话状态。
type Manager struct {
mu sync.RWMutex
clients map[string]*Client
sessions map[string]*agentSession
logger *zap.Logger
onMessage MessageHandler
onPong PongHandler
}
func NewManager(logger *zap.Logger) *Manager {
return &Manager{
clients: make(map[string]*Client),
sessions: make(map[string]*agentSession),
logger: logger,
}
}
func (m *Manager) SetMessageHandler(handler MessageHandler) {
m.onMessage = handler
}
func (m *Manager) SetPongHandler(handler PongHandler) {
m.onPong = handler
}
// Run 只负责周期性连接保活检查和进程退出清理;注册/注销本身是同步的,
// 注册响应拿到的 ACK 与刚完成的会话切换因此属于同一个原子操作。
func (m *Manager) Run(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
m.shutdown()
m.logger.Info("websocket manager stopped")
return
case <-ticker.C:
m.checkInactiveClients()
}
}
}
// Register 原子激活连接并返回该 BootID 会话已确认的累计位点。
// 同 BootID 重连复用处理队列;BootID 变化创建全新 session 并取消旧 session。
func (m *Manager) Register(client *Client, bootID string) uint64 {
var oldClient *Client
var oldSession *agentSession
var startSession *agentSession
m.mu.Lock()
oldClient = m.clients[client.ID]
session := m.sessions[client.ID]
if session == nil || (bootID != "" && session.bootID != bootID) {
oldSession = session
session = newAgentSession(client.ID, bootID)
m.sessions[client.ID] = session
startSession = session
}
client.session = session
m.clients[client.ID] = client
ackSeq := session.ackSeq.Load()
clientCount := len(m.clients)
m.mu.Unlock()
// map/session 指针已切换后再关闭旧对象。旧 worker 的提交路径会先
// 校验 session 指针,所以无法污染新会话。
if oldSession != nil {
oldSession.cancel()
}
if oldClient != nil && oldClient != client {
oldClient.close()
}
if startSession != nil {
go m.reliableProcessLoop(startSession)
go m.bestEffortProcessLoop(startSession)
}
m.logger.Info("agent connected",
zap.String("agentID", client.ID),
zap.String("bootID", bootID),
zap.Uint64("ackSeq", ackSeq),
zap.Int("totalClients", clientCount))
return ackSeq
}
// Unregister 只移除仍为 current 的连接,返回值用于决定是否标记离线。
func (m *Manager) Unregister(client *Client) bool {
m.mu.Lock()
current := m.clients[client.ID] == client
if current {
delete(m.clients, client.ID)
}
clientCount := len(m.clients)
m.mu.Unlock()
client.close()
if current {
m.logger.Info("agent disconnected",
zap.String("agentID", client.ID),
zap.Int("totalClients", clientCount))
}
return current
}
// ForgetAgent 删除已移除 agent 的连接与会话状态,终止常驻 worker。
func (m *Manager) ForgetAgent(agentID string) {
m.mu.Lock()
client := m.clients[agentID]
session := m.sessions[agentID]
delete(m.clients, agentID)
delete(m.sessions, agentID)
m.mu.Unlock()
if client != nil {
client.close()
}
if session != nil {
session.cancel()
}
}
func (m *Manager) shutdown() {
m.mu.Lock()
clients := make([]*Client, 0, len(m.clients))
for _, client := range m.clients {
clients = append(clients, client)
}
sessions := make([]*agentSession, 0, len(m.sessions))
for _, session := range m.sessions {
sessions = append(sessions, session)
}
m.clients = make(map[string]*Client)
m.sessions = make(map[string]*agentSession)
m.mu.Unlock()
for _, client := range clients {
client.close()
}
for _, session := range sessions {
session.cancel()
}
}
func (m *Manager) isCurrentSession(session *agentSession) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.sessions[session.agentID] == session
}
func (m *Manager) currentClientForSession(session *agentSession) *Client {
m.mu.RLock()
defer m.mu.RUnlock()
if m.sessions[session.agentID] != session {
return nil
}
client := m.clients[session.agentID]
if client == nil || client.session != session {
return nil
}
return client
}
func (m *Manager) checkInactiveClients() {
now := time.Now()
m.mu.RLock()
inactive := make([]*Client, 0)
for _, client := range m.clients {
if client.inactiveFor(now) > 2*time.Minute {
inactive = append(inactive, client)
}
}
m.mu.RUnlock()
for _, client := range inactive {
m.mu.RLock()
current := m.clients[client.ID] == client
m.mu.RUnlock()
if current {
m.logger.Warn("agent inactive timeout, disconnecting", zap.String("agentID", client.ID))
client.close()
}
}
}
func (m *Manager) SendToClient(agentID string, message []byte) error {
m.mu.RLock()
client := m.clients[agentID]
m.mu.RUnlock()
if client == nil {
return ErrClientNotFound
}
return client.sendWithTimeout(message, serverSendWait)
}
func (m *Manager) GetClient(agentID string) (*Client, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
client, ok := m.clients[agentID]
return client, ok
}
// DoIfCurrent 在连接仍为 current 的前提下执行状态变更,并用注册锁保证
// 回调完成前不会切换连接。用于 pong 在线写库,避免旧连接的迟到写入覆盖
// 新连接失败后的离线状态。
func (m *Manager) DoIfCurrent(client *Client, fn func() error) (bool, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.clients[client.ID] != client {
return false, nil
}
return true, fn()
}
func (m *Manager) GetAllClients() []string {
m.mu.RLock()
defer m.mu.RUnlock()
ids := make([]string, 0, len(m.clients))
for id := range m.clients {
ids = append(ids, id)
}
return ids
}
func (m *Manager) ClientCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.clients)
}
func (m *Manager) AckSeq(agentID string) uint64 {
m.mu.RLock()
session := m.sessions[agentID]
m.mu.RUnlock()
if session == nil {
return 0
}
return session.ackSeq.Load()
}
// enqueueBestEffort/ enqueueReliable 在持有 current-client 读锁时完成入队。
// Register 必须取得写锁,因此同 BootID 重连时,旧连接已经读出的消息必定
// 先入队,然后才会切换连接;切换完成后旧连接不能再把较大 seq 插到新连接
// 重放的较小 seq 前面,累计 ACK 顺序不会被跨连接竞态破坏。
func (m *Manager) enqueueBestEffort(client *Client, msg agentMessage) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if m.clients[client.ID] != client || m.sessions[client.ID] != client.session {
return false
}
select {
case client.session.bestEffortQueue <- msg:
return true
default:
return false
}
}
func (m *Manager) enqueueReliable(ctx context.Context, client *Client, msg agentMessage) error {
m.mu.RLock()
defer m.mu.RUnlock()
if m.clients[client.ID] != client || m.sessions[client.ID] != client.session {
return ErrClientNotFound
}
timer := time.NewTimer(reliableQueueWait)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-client.done:
return ErrClientNotFound
case <-client.session.ctx.Done():
return ErrClientNotFound
case client.session.reliableQueue <- msg:
return nil
case <-timer.C:
return ErrQueueFull
}
}
// ReadPump 只解析并按可靠性分流。可靠队列提供反压;best-effort 遥测
// 队列满时直接丢弃,避免 VM 慢写阻塞安全事件。
func (c *Client) ReadPump(ctx context.Context) {
defer c.close()
if c.conn == nil {
return
}
_ = c.conn.SetReadDeadline(time.Now().Add(serverPongWait))
c.conn.SetPongHandler(func(string) error {
if err := c.conn.SetReadDeadline(time.Now().Add(serverPongWait)); err != nil {
return err
}
c.touch()
if c.manager.onPong != nil {
// 同步执行,确保连接退出后的离线写库一定发生在本次 pong
// 状态刷新之后,避免遗留 goroutine 把已断开的连接重新标在线。
c.manager.onPong(c)
}
return nil
})
for {
_, raw, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
c.manager.logger.Error("websocket read error", zap.Error(err), zap.String("agentID", c.ID))
}
return
}
c.touch()
var msg protocol.InputMessage
if err := json.Unmarshal(raw, &msg); err != nil {
c.manager.logger.Error("failed to parse message", zap.Error(err), zap.String("agentID", c.ID))
continue
}
item := agentMessage{seq: msg.Seq, typ: string(msg.Type), data: msg.Data}
if msg.Seq == 0 {
if !c.manager.enqueueBestEffort(c, item) {
c.manager.logger.Warn("best-effort agent queue full, dropping telemetry",
zap.String("agentID", c.ID), zap.String("type", string(msg.Type)))
}
continue
}
if err := c.manager.enqueueReliable(ctx, c, item); err != nil {
c.manager.logger.Warn("reliable agent queue stalled, disconnecting for replay",
zap.String("agentID", c.ID), zap.Uint64("seq", msg.Seq), zap.Error(err))
return
}
}
}
func (m *Manager) bestEffortProcessLoop(session *agentSession) {
for {
select {
case <-session.ctx.Done():
return
case msg := <-session.bestEffortQueue:
if session.ctx.Err() != nil || !m.isCurrentSession(session) {
return
}
if m.onMessage == nil {
continue
}
if err := m.onMessage(session.ctx, session.agentID, msg.typ, msg.data); err != nil {
m.logger.Error("failed to handle best-effort message, dropped",
zap.Error(err), zap.String("agentID", session.agentID), zap.String("type", msg.typ))
}
}
}
}
func (m *Manager) reliableProcessLoop(session *agentSession) {
for {
select {
case <-session.ctx.Done():
return
case msg := <-session.reliableQueue:
if session.ctx.Err() != nil || !m.isCurrentSession(session) {
return
}
if msg.seq <= session.ackSeq.Load() {
m.sendAck(session, session.ackSeq.Load())
continue
}
if !m.processReliableMessage(session, msg) {
return
}
if !m.isCurrentSession(session) {
return
}
session.ackSeq.Store(msg.seq)
m.sendAck(session, msg.seq)
}
}
}
// processReliableMessage 对瞬时错误原位退避重试,保证后续序号不能越过
// 失败消息;永久格式错误明确丢弃并允许推进位点。
func (m *Manager) processReliableMessage(session *agentSession, msg agentMessage) bool {
retryDelay := messageRetryMin
for {
if session.ctx.Err() != nil || !m.isCurrentSession(session) {
return false
}
if m.onMessage == nil {
return true
}
err := m.onMessage(session.ctx, session.agentID, msg.typ, msg.data)
if err == nil {
return true
}
if isPermanent(err) {
m.logger.Error("permanent reliable message error, dropped",
zap.Error(err), zap.String("agentID", session.agentID),
zap.String("type", msg.typ), zap.Uint64("seq", msg.seq))
return true
}
m.logger.Warn("reliable message handling failed, retrying",
zap.Error(err), zap.String("agentID", session.agentID),
zap.String("type", msg.typ), zap.Uint64("seq", msg.seq),
zap.Duration("retryAfter", retryDelay))
timer := time.NewTimer(retryDelay)
select {
case <-session.ctx.Done():
timer.Stop()
return false
case <-timer.C:
}
if retryDelay < messageRetryMax {
retryDelay *= 2
if retryDelay > messageRetryMax {
retryDelay = messageRetryMax
}
}
}
}
func (m *Manager) sendAck(session *agentSession, seq uint64) {
payload, err := json.Marshal(protocol.OutboundMessage{
Type: protocol.MessageTypeAck,
Data: protocol.AckData{Seq: seq},
})
if err != nil {
return
}
if client := m.currentClientForSession(session); client != nil {
client.trySend(payload)
}
}
func (c *Client) WritePump() {
if c.conn == nil {
return
}
ticker := time.NewTicker(serverPingInterval)
defer ticker.Stop()
for {
select {
case <-c.done:
return
case message := <-c.send:
_ = c.conn.SetWriteDeadline(time.Now().Add(serverWriteWait))
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
c.manager.logger.Error("failed to write message", zap.Error(err), zap.String("agentID", c.ID))
c.close()
return
}
case <-ticker.C:
_ = c.conn.SetWriteDeadline(time.Now().Add(serverWriteWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
c.close()
return
}
}
}
}
func (c *Client) ShouldWriteStatus(refreshInterval time.Duration) bool {
now := time.Now().UnixMilli()
last := c.lastStatusWrite.Load()
return last == 0 || now-last >= refreshInterval.Milliseconds()
}
func (c *Client) MarkStatusWritten() {
c.lastStatusWrite.Store(time.Now().UnixMilli())
}
var (
ErrClientNotFound = &websocket.CloseError{Code: 1000, Text: "client not found"}
ErrSendTimeout = &websocket.CloseError{Code: 1001, Text: "send timeout"}
ErrQueueFull = &websocket.CloseError{Code: 1013, Text: "reliable queue full"}
)