Documentation
¶
Index ¶
- Constants
- func ClassifyCloseError(err error) (closeCode int, isNormal bool)
- func ClientMatchesEnvelope(client *models.Client, msgAppID, msgNamespace string, msgGroupIDs []string) bool
- func CopyClientsFromMap(clientMap map[string]*models.Client) []*models.Client
- func GetClientIP(client *models.Client) string
- func IsControlMessage(mt models.MessageType) bool
- func IsDisconnectMessage(mt models.MessageType) bool
- type CapacityManager
- type ControlLane
- type DisconnectionSubmitter
- type EvictHook
- type HeartbeatHost
- type HeartbeatManager
- type LifecycleHost
- type LifecycleManager
- func (m *LifecycleManager) CleanupHalfRegistered(client *models.Client)
- func (m *LifecycleManager) CloseChannel(client *models.Client)
- func (m *LifecycleManager) CloseConnection(client *models.Client)
- func (m *LifecycleManager) EnforceMultiLoginPolicy(newClient *models.Client)
- func (m *LifecycleManager) KickClient(client *models.Client, reason models.DisconnectReason, message string)
- func (m *LifecycleManager) KickUser(ctx context.Context, userID string, reason string, sendNotification bool, ...) *models.KickUserResult
- func (m *LifecycleManager) RemoveUnsafe(client *models.Client)
- type MultiLoginPolicy
- type RecordHost
- type RecordManager
- func (m *RecordManager) Create(client *models.Client) *models.ConnectionRecord
- func (m *RecordManager) MarkDisconnected(client *models.Client)
- func (m *RecordManager) MarkDisconnectedBatch(clients []*models.Client) []*models.DisconnectionEntry
- func (m *RecordManager) Save(ctx context.Context, record *models.ConnectionRecord)
- type RegistryCapacity
- type ShardedRegistry
- func (r *ShardedRegistry) AddClient(client *models.Client)
- func (r *ShardedRegistry) AgentEnabled() bool
- func (r *ShardedRegistry) Clear()
- func (r *ShardedRegistry) FilterClients(predicate func(*models.Client) bool) []*models.Client
- func (r *ShardedRegistry) ForEachClient(fn func(clientID string, client *models.Client) bool)
- func (r *ShardedRegistry) ForEachClientFiltered(msgAppID, msgNamespace string, msgGroupIDs []string, ...)
- func (r *ShardedRegistry) ForEachClientFilteredParallel(workerNum int, msgAppID, msgNamespace string, msgGroupIDs []string, ...)
- func (r *ShardedRegistry) ForEachClientParallel(workerNum int, fn func(clientID string, client *models.Client))
- func (r *ShardedRegistry) ForEachObserver(fn func(userID string, clientID string, client *models.Client) bool)
- func (r *ShardedRegistry) ForEachSSEClient(fn func(userID string, clientID string, client *models.Client) bool)
- func (r *ShardedRegistry) ForEachSSEClientParallel(workerNum int, fn func(userID, clientID string, client *models.Client))
- func (r *ShardedRegistry) ForEachSSEUserClient(userID string, fn func(clientID string, client *models.Client) bool)
- func (r *ShardedRegistry) ForEachUser(fn func(userID string, clients map[string]*models.Client) bool)
- func (r *ShardedRegistry) ForEachUserClient(userID string, fn func(clientID string, client *models.Client) bool)
- func (r *ShardedRegistry) ForEachUserClientFiltered(userID, msgAppID, msgNamespace string, msgGroupIDs []string, ...)
- func (r *ShardedRegistry) GetActiveClientCount() int64
- func (r *ShardedRegistry) GetAgentUserClients(userID string) (map[string]*models.Client, bool)
- func (r *ShardedRegistry) GetAgentUserCount() int
- func (r *ShardedRegistry) GetAllClients() []*models.Client
- func (r *ShardedRegistry) GetClient(clientID string) (*models.Client, bool)
- func (r *ShardedRegistry) GetClientCount() int64
- func (r *ShardedRegistry) GetClientIPs(ctx context.Context, userID string) []string
- func (r *ShardedRegistry) GetClientMetadata(clientID string, key string) (interface{}, bool)
- func (r *ShardedRegistry) GetClientStats(clientID string) map[string]interface{}
- func (r *ShardedRegistry) GetClientsByDepartmentGrouped() map[models.Department][]*models.Client
- func (r *ShardedRegistry) GetClientsByStatusGrouped() map[models.UserStatus][]*models.Client
- func (r *ShardedRegistry) GetClientsByUserID(ctx context.Context, userID string) []*models.Client
- func (r *ShardedRegistry) GetClientsByUserTypeGrouped() map[models.UserType][]*models.Client
- func (r *ShardedRegistry) GetClientsCopyForUser(ctx context.Context, userID, clientID string) []*models.Client
- func (r *ShardedRegistry) GetClientsWithStatus(status models.UserStatus) []*models.Client
- func (r *ShardedRegistry) GetLocalGroupMembers(appID string, groupIDs []string) []string
- func (r *ShardedRegistry) GetLocalNamespaceUsers(appID, namespace string) []string
- func (r *ShardedRegistry) GetLocalUsersByClientType(appID string, clientType models.ClientType) ([]string, bool)
- func (r *ShardedRegistry) GetLocalUsersByDepartment(appID string, dept models.Department) ([]string, bool)
- func (r *ShardedRegistry) GetLocalUsersByRole(appID string, role models.UserRole) ([]string, bool)
- func (r *ShardedRegistry) GetLocalUsersByUserType(appID string, userType models.UserType) ([]string, bool)
- func (r *ShardedRegistry) GetMostRecentClient(ctx context.Context, userID string) *models.Client
- func (r *ShardedRegistry) GetObserverDeviceCount() int
- func (r *ShardedRegistry) GetObserverUserClients(userID string) (map[string]*models.Client, bool)
- func (r *ShardedRegistry) GetObserverUserCount() int
- func (r *ShardedRegistry) GetObserversForMessage(namespace string, groupIDs ...string) []*models.Client
- func (r *ShardedRegistry) GetOnlineUserCountByType(userType models.UserType) (int64, error)
- func (r *ShardedRegistry) GetOnlineUserIDs() []string
- func (r *ShardedRegistry) GetOnlineUsersByType(userType models.UserType) ([]string, error)
- func (r *ShardedRegistry) GetSSEClientCount() int64
- func (r *ShardedRegistry) GetSSEUserClients(userID string) (map[string]*models.Client, bool)
- func (r *ShardedRegistry) GetSSEUserCount() int
- func (r *ShardedRegistry) GetSSEUserIDs() []string
- func (r *ShardedRegistry) GetUserClientCount(userID string) int
- func (r *ShardedRegistry) GetUserClients(userID string) (map[string]*models.Client, bool)
- func (r *ShardedRegistry) GetUserClientsCopy() []*models.Client
- func (r *ShardedRegistry) GetUserCount() int64
- func (r *ShardedRegistry) GetUserStatus(ctx context.Context, userID string) models.UserStatus
- func (r *ShardedRegistry) HasAgent(userID string) bool
- func (r *ShardedRegistry) HasObserver(userID string) bool
- func (r *ShardedRegistry) HasSSEUser(userID string) bool
- func (r *ShardedRegistry) HasUser(userID, appID, namespace string) bool
- func (r *ShardedRegistry) HasUserClient(ctx context.Context, userID string) bool
- func (r *ShardedRegistry) HasUserExactEnvelope(userID, appID, namespace string) bool
- func (r *ShardedRegistry) ObserverEnabled() bool
- func (r *ShardedRegistry) RemoveClient(clientID, userID string) *models.Client
- func (r *ShardedRegistry) UpdateClientMetadata(clientID string, key string, value interface{}) error
- type SlowConsumerScanner
Constants ¶
const DefaultClientSendChanCapacity = 256
DefaultClientSendChanCapacity 默认客户端 SendChan 缓冲区容量(当配置不存在时使用)
Variables ¶
This section is empty.
Functions ¶
func ClassifyCloseError ¶
ClassifyCloseError 分类关闭错误
func ClientMatchesEnvelope ¶
func ClientMatchesEnvelope(client *models.Client, msgAppID, msgNamespace string, msgGroupIDs []string) bool
ClientMatchesEnvelope 判断某个 client 是否应该接收携带指定路由信封的消息
投递匹配规则:
- appId 维度(最上层隔离):严格相等,无空值兼容(appID 无全局语义,入口层必归一化为 DefaultAppID)
- namespace 维度:msgNamespace 非空时严格相等;msgNamespace=="" 表示全局广播,跳过 ns 过滤匹配所有
设计说明:
- appID 严格匹配:appID 是应用隔离维度,不存在"全局跨 app 广播"语义,空值在入口层归一化为 DefaultAppID
- namespace 空值兼容:全局广播(跨所有命名空间的系统通知)是合法业务需求, msgNamespace=="" 表示全局广播,匹配所有 ns 的 client(与 Broadcast 全局语义一致)
⚠️ 不做 msgGroupIDs vs client.GroupID 匹配(与 ForEachUserClientFiltered 对称设计):
- msg.GroupIDs 存放的是"业务群组ID"(如 g-broadcast),而 client.GroupID 是"连接级系统组" (如 __default_gp__),两者完全两个维度,强行匹配导致客户端全部被过滤(delivered=0)
- 群组成员过滤已由 groupRepo.GetMembers + memberSet 完成(broadcastToUserIDs 路径), 全局广播(broadcastToFiltered/ForEachClientFiltered)无需也不应做系统组匹配
- msgGroupIDs 参数仅作签名兼容,不参与匹配
本函数为热路径(每次投递遍历 client 都调用):零分配、内联友好、短路优先
func CopyClientsFromMap ¶
CopyClientsFromMap 从客户端映射中复制客户端列表 用于避免在遍历时map被修改导致的数据竞争
func IsControlMessage ¶
func IsControlMessage(mt models.MessageType) bool
IsControlMessage 判断是否为控制类消息(控制通道 + 兜底降级的路由依据)
func IsDisconnectMessage ¶
func IsDisconnectMessage(mt models.MessageType) bool
IsDisconnectMessage 判断是否为断链类控制消息(CtrlCh 满时降级为直接断链)
Types ¶
type CapacityManager ¶
type CapacityManager struct {
// contains filtered or unexported fields
}
CapacityManager 按 UserType 配置通道容量并初始化客户端通道四件套 (DoneCh/PongCh/CtrlCh/SendChan);channel 与 Client 同生命周期,随连接分配、断连后由 GC 回收
func NewCapacityManager ¶
func NewCapacityManager(cfg *wscconfig.ClientCapacity, logger spi.Logger) *CapacityManager
NewCapacityManager 创建容量管理器 cfg 为 nil 时使用 wscconfig.DefaultClientCapacity();logger 为 nil 时使用默认日志器
func (*CapacityManager) CapacityFor ¶
func (cm *CapacityManager) CapacityFor(client *models.Client) int
CapacityFor 根据客户端 UserType 获取 SendChan 缓冲区容量(配置不存在项为 0 时由上层兜底)
func (*CapacityManager) InitClientSendChan ¶
func (cm *CapacityManager) InitClientSendChan(client *models.Client)
InitClientSendChan 初始化客户端的通道四件套(按 UserType 容量分配 SendChan) 在客户端注册时调用,确保 SendChan 使用正确的缓冲区大小
type ControlLane ¶
type ControlLane struct {
// contains filtered or unexported fields
}
ControlLane 控制通道投递器(必达级消息的独立 lane)
func NewControlLane ¶
func NewControlLane(logger spi.Logger) *ControlLane
NewControlLane 创建控制通道投递器(logger 为 nil 时使用默认日志器)
func (*ControlLane) SendControl ¶
func (l *ControlLane) SendControl(client *models.Client, data []byte, msg *models.HubMessage) bool
SendControl 向客户端控制通道发送已序列化的必达级消息
降级链(逐级兜底,控制消息尽力可达):
- CtrlCh 投递(首选:独立 lane,不受数据洪峰影响)
- CtrlCh 未初始化(手工构造/SSE 早期客户端)→ 回退数据 lane TrySend (无优先保障但可达——比直接断链后客户端不知原因好)
- 通道满且断链类(KickOut/ForceOffline/Terminate):直接 Conn.Close() (断链本就是目的,通知帧是锦上添花)
- 通道满且非断链类:返回 false,调用方走各自兜底(ACK 有重试+超时转离线)
func (*ControlLane) SendControlMessage ¶
func (l *ControlLane) SendControlMessage(client *models.Client, msg *models.HubMessage) bool
SendControlMessage 向客户端控制通道发送控制类消息(内部序列化 + 控制通道投递一体化)
用于 KickOut/ForceOffline 等场景:消息经 Marshal 后走 CtrlCh,绕开业务 SendChan 洪峰
type DisconnectionSubmitter ¶
type DisconnectionSubmitter interface {
// Submit 非阻塞提交断连终态条目(队列满时返回 false,
// 与记录池可丢弃语义一致,仅影响审计统计精度)
Submit(entry *models.DisconnectionEntry) bool
}
DisconnectionSubmitter 断连终态攒批提交端口(消费者侧端口:Hub 实现)
正常注销路径的断连终态经攒批合并落库(CASE WHEN 单 SQL),替代逐条 UPDATE + 每断一连一个超时 goroutine 直打 DB 的写放大路径
type EvictHook ¶
type EvictHook interface {
OnSlowConsumerEvicted(client *models.Client, consecutive int, ratio float64)
}
EvictHook 驱逐回调端口(消费者侧端口:Hub 实现,扫描器不感知驱逐细节)
实现方职责(见 hub 编排层):
- 慢消费者驱逐埋点(overload 域指标)
- KickOut 控制消息通知客户端驱逐理由
- Unregister 断链(断链不丢消息)
type HeartbeatHost ¶
type HeartbeatHost interface {
// Context 返回 Hub 生命周期上下文(仅用于日志关联)
Context() context.Context
// GetLogger 日志器
GetLogger() spi.Logger
// Unregister 异步注销客户端(心跳超时与 SSE 兜底触发断链)
Unregister(client *models.Client)
// TrackHeartbeatStats 心跳统计追踪(stats 域漏斗,不阻塞主流程)
TrackHeartbeatStats(client *models.Client)
// EnqueueHeartbeatRenew 心跳 Redis 在线索引续期入队(满则丢弃,下次心跳补投)
EnqueueHeartbeatRenew(client *models.Client)
// GetBeforeHeartbeatCallback 心跳前置回调(返回 false 跳过后续心跳处理)
GetBeforeHeartbeatCallback() func(client *models.Client) bool
// GetHeartbeatReportCallback 心跳上报回调(业务侧按心跳周期感知活跃度)
GetHeartbeatReportCallback() func(client *models.Client)
// GetAfterHeartbeatCallback 心跳后置回调
GetAfterHeartbeatCallback() func(client *models.Client)
// GetHeartbeatTimeoutCallback 心跳超时回调(超时注销时触发)
GetHeartbeatTimeoutCallback() func(clientID string, userID string, lastHeartbeat time.Time)
}
HeartbeatHost 心跳子域所需的 Hub 能力面
心跳链路(前置拦截 → 时间轮续期 → Redis 续期入队 → 回调链 → 统计追踪) 的外部依赖经本端口注入,由 hub 编排层实现;回调 getter 用结构类型声明, 避免 hub ⇄ connection 循环 import(与 group.Host 回调端口同款约定), 未注册时返回 nil,调用方跳过(不可假定非空)
type HeartbeatManager ¶
type HeartbeatManager struct {
// contains filtered or unexported fields
}
HeartbeatManager 心跳管理器(连接域)
持有分片时间轮,管理 WebSocket 客户端的 O(1) 心跳超时;SSE 客户端 不发送 PING,无法通过时间轮 Refresh,由 ScanSSETimeouts 周期扫描 LastSeen 判断活跃度
func NewHeartbeatManager ¶
func NewHeartbeatManager(host HeartbeatHost, registry *ShardedRegistry, clientTimeout time.Duration, timerOptions ...syncx.TimerOption) *HeartbeatManager
NewHeartbeatManager 构造心跳管理器
构造期初始化时间轮(不依赖 Run),确保 Schedule/Refresh/Cancel 在 任何 goroutine 启动前可用;timerOptions 透传时间轮分片配置
func (*HeartbeatManager) CancelTimeout ¶
func (m *HeartbeatManager) CancelTimeout(clientID string)
CancelTimeout 注销连接时取消时间轮上的心跳超时任务
func (*HeartbeatManager) Handle ¶
func (m *HeartbeatManager) Handle(client *models.Client)
Handle 处理一次心跳(协议级 PING 与应用层心跳共用同一保活入口) 流程:前置回调 → 续期 → Redis 异步续期入队 → 上报回调 → 后置回调 → 统计
日志策略:成功路径单条 Debug(生产 Info 级静默,排查时开 Debug 可观测心跳节奏; 千万级下逐条 Info 是日志风暴,心跳频次经 TrackHeartbeatStats 聚合统计)
func (*HeartbeatManager) ScanSSETimeouts ¶
func (m *HeartbeatManager) ScanSSETimeouts()
ScanSSETimeouts 扫描 SSE 客户端心跳超时(兜底机制) WebSocket 客户端由时间轮 O(1) 管理,此处仅扫描 SSE 客户端 SSE 客户端不发送 PING,无法通过时间轮 Refresh,需定期扫描 LastSeen 判断活跃度
⚠️ 死锁防御:遍历持有 shard 读锁,先收集超时客户端到本地 slice(mutex 保护 并发 append),遍历结束后在锁外统一调用 Unregister
func (*HeartbeatManager) ScheduleTimeout ¶
func (m *HeartbeatManager) ScheduleTimeout(client *models.Client)
ScheduleTimeout 注册连接时在时间轮上调度心跳超时任务 仅用于 WebSocket 客户端;SSE 客户端由 ScanSSETimeouts 扫描兜底
type LifecycleHost ¶
type LifecycleHost interface {
// GetLogger 日志器
GetLogger() spi.Logger
// IsShuttingDown 编排层是否正在关闭(决定断链是否先发 1001 GoingAway 控制帧)
IsShuttingDown() bool
// Unregister 异步注销客户端(踢出路径断链)
Unregister(client *models.Client)
// SendToClient 经消息域向客户端投递(强制下线通知等控制类直达消息)
SendToClient(ctx context.Context, client *models.Client, msg *models.HubMessage)
}
LifecycleHost 连接生命周期子域所需的 Hub 能力面
多端登录治理与踢出断链的外部依赖经本端口注入,由 hub 编排层实现
type LifecycleManager ¶
type LifecycleManager struct {
// contains filtered or unexported fields
}
LifecycleManager 连接生命周期管理器(连接域)
覆盖注册后的准入治理(多端登录策略)与断链执行(踢出通知、 精简移除、半注册清理、通道/连接关闭原语)
func NewLifecycleManager ¶
func NewLifecycleManager(host LifecycleHost, registry *ShardedRegistry, heartbeat *HeartbeatManager, policy MultiLoginPolicy) *LifecycleManager
NewLifecycleManager 构造连接生命周期管理器
func (*LifecycleManager) CleanupHalfRegistered ¶
func (m *LifecycleManager) CleanupHalfRegistered(client *models.Client)
CleanupHalfRegistered 清理半注册连接(注册编排 panic 兜底) 半注册状态:连接已加入注册表 + 心跳超时任务已调度,但注册流程中断 幂等安全:各清理步骤对"未执行到"的步骤均为无操作
func (*LifecycleManager) CloseChannel ¶
func (m *LifecycleManager) CloseChannel(client *models.Client)
CloseChannel 关闭客户端发送通道 用 DoneCh 通知写循环退出,数据通道(SendChan/SSEMessageCh)永不 close:
- 消除 TrySend 的 chansend 与本函数 closechan 的数据竞态
- 不跨连接复用:竞态窗口内 racing sender 仍可能写入残留消息,复用会跨连接串消息; channel 与 Client 同生命周期,断连后随 Client 整体交由 GC 回收
- 不置 nil SendChan,避免与写循环 select 读产生数据竞争
func (*LifecycleManager) CloseConnection ¶
func (m *LifecycleManager) CloseConnection(client *models.Client)
CloseConnection 关闭 WebSocket 连接 Hub 关闭(如 K8s 滚动更新)时先发送 1001 GoingAway 控制帧, 让客户端识别为服务端主动离开并触发重连,而不是收到 1006 异常断开
func (*LifecycleManager) EnforceMultiLoginPolicy ¶
func (m *LifecycleManager) EnforceMultiLoginPolicy(newClient *models.Client)
EnforceMultiLoginPolicy 统一处理多端登录策略 根据策略决定是否允许多端登录、是否限制连接数:
- AllowMultiLogin=false:踢掉该用户全部旧连接(同信封快速检查,无旧连接零开销)
- MaxConnectionsPerUser>0:达到上限时踢掉最不活跃(最旧心跳)的旧连接
调用时新连接已入注册表(AddClient 前置),故踢人时排除新连接自身; 同 clientID 覆盖已由注销路径的指针一致性校验保护。 多端登录策略按 appID+namespace 信封隔离:不同应用/命名空间的连接互不影响 (app-A 的连接数不挤占 app-B 的配额)
func (*LifecycleManager) KickClient ¶
func (m *LifecycleManager) KickClient(client *models.Client, reason models.DisconnectReason, message string)
KickClient 踢掉客户端并发送强制下线通知 通知先于断开写入客户端发送通道,写循环异步发出;随后注销连接
func (*LifecycleManager) KickUser ¶
func (m *LifecycleManager) KickUser(ctx context.Context, userID string, reason string, sendNotification bool, notificationMsg string) *models.KickUserResult
KickUser 统一踢出用户全部连接(唯一踢人实现,按 ctx 路由信封 appID+namespace 隔离)
收集维度:按 ctx 路由信封过滤(与 P2P 投递同一隔离语义)——同名 userID 跨 app/namespace 多端在线时,仅踢出信封内的连接,不同应用/租户互不误踢; gRPC/distributed 路径经路由信封恢复/注入后传入,本地路径继承调用方信封
幂等语义:用户已无连接(收集数 0)即"已离线"目标达成,结果 KickedConnections=0 不视为失败;sendNotification=true 时先向全部连接写入 KickOut 通知再注销 (Guaranteed 级控制消息,断链前投递,notificationMsg 为通知文案)
ctx 由调用方传入(grpc/distributed 路径已恢复 trace_id),实现全链路追踪
func (*LifecycleManager) RemoveUnsafe ¶
func (m *LifecycleManager) RemoveUnsafe(client *models.Client)
RemoveUnsafe 从注册表移除客户端(shutdown 路径专用精简清理) 仅做:注册表移除 + 时间轮取消 + 指针一致性校验 + 关闭通道与连接; 不触发回调与逐条记录落库(由停机批量清理统一处理, 避免大量串行写 Redis/DB 导致 shutdown 超时)
type MultiLoginPolicy ¶
type MultiLoginPolicy struct {
// AllowMultiLogin 是否允许同一用户多端在线(false:新连接踢掉全部旧连接)
AllowMultiLogin bool
// MaxConnectionsPerUser 单用户连接数上限(0 表示不限制;达上限踢最旧连接)
MaxConnectionsPerUser int
}
MultiLoginPolicy 多端登录策略快照(编排层构造期从全局配置提取)
type RecordHost ¶
type RecordHost interface {
// GetLogger 日志器
GetLogger() spi.Logger
// GetConnectionRecordRepo 连接记录仓储(未注入返回 nil,调用方自行降级)
GetConnectionRecordRepo() spi.ConnectionStore
// GetConnectionQualityRepository 连接质量仓储(未注入返回 nil,调用方自行降级):
// 注册落库时与 connect 行 1:1 同生 quality 初始行,batcher 的心跳/统计/错误
// 批量 UPDATE 才有落点(无初始行则全部空转影响 0 行)
GetConnectionQualityRepository() spi.ConnectionQualityStore
// GetDisconnectionBatcher 断连终态攒批器(未注入返回 nil,调用方自行降级)
GetDisconnectionBatcher() DisconnectionSubmitter
}
RecordHost 连接记录子域所需的 Hub 能力面
仓储经端口动态读取而非构造期快照:SetConnectionRecordRepository 支持 运行期装配(NewHub 后经 adapter 注入),每次读写实时反映注入状态
type RecordManager ¶
type RecordManager struct {
// contains filtered or unexported fields
}
RecordManager 连接记录管理器(连接域)
覆盖连接记录的构造(Client → ConnectionRecord 快照)与终态写路径 (注册异步保存 / 注销异步标记断开 / 停机批量终态)
func NewRecordManager ¶
func NewRecordManager(host RecordHost) *RecordManager
NewRecordManager 构造连接记录管理器
func (*RecordManager) Create ¶
func (m *RecordManager) Create(client *models.Client) *models.ConnectionRecord
Create 构造连接记录(内存对象,供异步保存 + 连接回调使用)
func (*RecordManager) MarkDisconnected ¶
func (m *RecordManager) MarkDisconnected(client *models.Client)
MarkDisconnected 标记连接为已断开(攒批路径,batcher 未注入时 no-op) 构造 DisconnectionEntry 快照提交到 DisconnectionBatcher,由后台攒批合并为 CASE WHEN 单 SQL 落库;快照在断连瞬间冻结 DisconnectedAt/ConnectedAt, flush 延迟不虚增 duration;队列满丢弃与原记录池可丢弃语义一致
func (*RecordManager) MarkDisconnectedBatch ¶
func (m *RecordManager) MarkDisconnectedBatch(clients []*models.Client) []*models.DisconnectionEntry
MarkDisconnectedBatch 停机批量标记连接断开(仓储未注入时返回 nil) 与单连接路径的差异:reason 为 ServerShutdown + 关闭码 1001(客户端据此识别服务端主动离开并重连); 不走攒批队列而是分块同步直调 BatchMarkDisconnected—— 停机路径需在本方法返回后立即被批量终评读 duration 算分,保序优先 返回构造的 entries 供终评直接复用 duration(与 SQL 写入值同源同公式),免去终评再把刚落库的 duration 读回来的往返
func (*RecordManager) Save ¶
func (m *RecordManager) Save(ctx context.Context, record *models.ConnectionRecord)
Save 保存或更新连接记录到数据库(仓储未注入时 no-op) ctx 应为 client.Context(带 client 维度的 trace_id),实现异步保存的全链路追踪 connect 身份行与 quality 初始行同批落库:batcher 的心跳/统计/错误批量 UPDATE 以 quality 行存在为前提,无初始行则全部空转影响 0 行(曾致质量表长期零数据)
type RegistryCapacity ¶
type RegistryCapacity struct {
// TotalClients 总客户端连接数预估(主存储 userShards)
TotalClients int
// SSEClients SSE 客户端连接数预估(sseShards)
SSEClients int
// ObserverClients 观察者连接数预估(observerShards)
ObserverClients int
// AgentClients 客服/Bot 连接数预估(agentShards)
AgentClients int
}
RegistryCapacity 容量提示(用于预分配各 ShardedMap 的每 shard 内部 map 容量)
设计思路:
- 总容量除以 shardCount 得到每 shard 提示,减少 map 扩容次数
- 各分类索引(SSE/Observer/Agent)按预估占比独立分配,避免过度预分配
- 0 表示不预分配,由 map 自适应扩容
type ShardedRegistry ¶
type ShardedRegistry struct {
// contains filtered or unexported fields
}
ShardedRegistry 分片注册表 基于 syncx.ShardedMap 实现,替代 Hub 的单一 mutex + map 结构 同时承担原本由 Hub 维护的 sseClients/observerClients/agentClients 分类索引
func NewShardedRegistry ¶
func NewShardedRegistry(agentEnabled, observerEnabled bool, capacity RegistryCapacity) *ShardedRegistry
NewShardedRegistry 创建分片注册表 agentEnabled / observerEnabled 控制是否启用对应分类分片; 未启用的分类 shards 保持 nil,相关读写直接跳过,避免无谓内存与锁开销
capacity 提供总容量预估,用于预分配各 ShardedMap 的每 shard 内部 map 容量; 传入零值 RegistryCapacity 时退化为不预分配(与旧版兼容)
func (*ShardedRegistry) AddClient ¶
func (r *ShardedRegistry) AddClient(client *models.Client)
AddClient 添加客户端到注册表 主存储 + 分类索引在同一 userID 的 shard 锁内原子完成(ShardedMap 保证同一 key 落同一 shard)
覆盖语义(断线重连场景):当相同 clientID 已存在时,仅更新主存储与分类索引的指针, 不重复累加 clientCount/sseCount,否则计数器会随每次重连持续膨胀、与实际连接数脱节 observerUserCount/agentUserCount 在各自 add 方法内已按"用户是否新增"判定,无需此处处理
func (*ShardedRegistry) AgentEnabled ¶
func (r *ShardedRegistry) AgentEnabled() bool
AgentEnabled 返回客服模块是否启用(shards 非 nil)
func (*ShardedRegistry) FilterClients ¶
FilterClients 按条件过滤客户端(零拷贝:ForEachClient 遍历 + 条件收集,避免 GetClientsCopy 全量拷贝)
func (*ShardedRegistry) ForEachClient ¶
func (r *ShardedRegistry) ForEachClient(fn func(clientID string, client *models.Client) bool)
ForEachClient 遍历所有客户端 回调返回 false 时停止遍历 注意:回调在持有读锁时执行,不应执行耗时操作
func (*ShardedRegistry) ForEachClientFiltered ¶
func (r *ShardedRegistry) ForEachClientFiltered(msgAppID, msgNamespace string, msgGroupIDs []string, fn func(clientID string, client *models.Client) bool)
ForEachClientFiltered 遍历注册表所有 client,仅对匹配路由信封(含 appId)的 client 调用 fn 用于全局广播等场景:app1/ns1 的广播只投递给 app1/ns1 的所有在线设备
func (*ShardedRegistry) ForEachClientFilteredParallel ¶
func (r *ShardedRegistry) ForEachClientFilteredParallel(workerNum int, msgAppID, msgNamespace string, msgGroupIDs []string, fn func(clientID string, client *models.Client))
ForEachClientFilteredParallel 并行遍历所有客户端,仅对匹配路由信封(含 appId)的 client 调用 fn 用于全局广播等场景:app1/ns1 的广播只投递给 app1/ns1 的所有在线设备 基于 userShards.RangeParallel 直接在 shard 级别并行遍历,零额外分配 workerNum <= 0 时使用 GOMAXPROCS;用户数低于 parallelThreshold 时退化为串行
func (*ShardedRegistry) ForEachClientParallel ¶
func (r *ShardedRegistry) ForEachClientParallel(workerNum int, fn func(clientID string, client *models.Client))
ForEachClientParallel 并行遍历所有客户端(百万级连接优化) 基于 userShards.RangeParallel 直接在 shard 级别并行遍历,零额外分配 fn 在 shard 读锁内执行(并发安全),回调应为非阻塞操作 workerNum <= 0 时使用 GOMAXPROCS;用户数低于 parallelThreshold 时退化为串行
func (*ShardedRegistry) ForEachObserver ¶
func (r *ShardedRegistry) ForEachObserver(fn func(userID string, clientID string, client *models.Client) bool)
ForEachObserver 遍历所有观察者客户端
func (*ShardedRegistry) ForEachSSEClient ¶
func (r *ShardedRegistry) ForEachSSEClient(fn func(userID string, clientID string, client *models.Client) bool)
ForEachSSEClient 遍历所有 SSE 客户端(用于广播) 回调返回 false 时停止遍历
func (*ShardedRegistry) ForEachSSEClientParallel ¶
func (r *ShardedRegistry) ForEachSSEClientParallel(workerNum int, fn func(userID, clientID string, client *models.Client))
ForEachSSEClientParallel 并行遍历所有 SSE 客户端(百万级连接优化) 基于 sseShards.RangeParallel 直接在 shard 级别并行遍历,零额外分配 workerNum <= 0 时使用 GOMAXPROCS;用户数低于 parallelThreshold 时退化为串行
func (*ShardedRegistry) ForEachSSEUserClient ¶
func (r *ShardedRegistry) ForEachSSEUserClient(userID string, fn func(clientID string, client *models.Client) bool)
ForEachSSEUserClient 遍历指定用户的所有 SSE 客户端(持读锁,零拷贝) 回调返回 false 时停止遍历 替代 GetSSEUserClients 锁外遍历内部 map 的数据竞争
func (*ShardedRegistry) ForEachUser ¶
func (r *ShardedRegistry) ForEachUser(fn func(userID string, clients map[string]*models.Client) bool)
ForEachUser 遍历所有在线用户 回调返回 false 时停止遍历 注意:回调在持有读锁时执行,不应执行耗时操作
func (*ShardedRegistry) ForEachUserClient ¶
func (r *ShardedRegistry) ForEachUserClient(userID string, fn func(clientID string, client *models.Client) bool)
ForEachUserClient 遍历指定用户的所有客户端(在读锁内执行,并发安全) 回调返回 false 时停止遍历回调不应执行阻塞操作(TrySend 等非阻塞操作可安全调用)
func (*ShardedRegistry) ForEachUserClientFiltered ¶
func (r *ShardedRegistry) ForEachUserClientFiltered(userID, msgAppID, msgNamespace string, msgGroupIDs []string, fn func(clientID string, client *models.Client) bool)
ForEachUserClientFiltered 遍历指定 userID 的所有在线设备,仅对匹配路由信封(含 appId)的 client 调用 fn fn 返回 false 时中止遍历(与 ForEachUserClient 语义一致) 用于 P2P/群组的定向投递:即使同名 userID 跨 app/namespace 同时在线,也只投递给路由匹配的设备
appId/namespace 匹配规则(复用 ClientMatchesEnvelope,保持单一真相源):
- appId 严格相等(入口层已归一化为 DefaultAppID,无空值兼容)
- namespace 非空时严格相等;msgNamespace=="" 表示全局广播,跳过 ns 过滤匹配所有
设计说明:只做 appId+namespace 隔离,**不对 msgGroupIDs vs client.GroupID 做系统组匹配**
- 场景1(P2P 点对点):调用方已明确指定 userID 为接收者,msg.GroupIDs=nil → 无 group 维度
- 场景2(群组消息):调用方已通过 group_repo.GetMembers() 获取业务群组成员 userIDs, msg.GroupIDs 存放的是"业务群组ID"(如 "g-broadcast"),而 client.GroupID 是"连接级系统组" (如 __default_gp__),两者完全两个维度,强行匹配导致群成员 device 全部被过滤(delivered=0)
func (*ShardedRegistry) GetActiveClientCount ¶
func (r *ShardedRegistry) GetActiveClientCount() int64
GetActiveClientCount 获取活跃(非 SSE)客户端数 = 总连接数 - SSE 连接数
func (*ShardedRegistry) GetAgentUserClients ¶
GetAgentUserClients 获取指定客服的所有客户端
func (*ShardedRegistry) GetAgentUserCount ¶
func (r *ShardedRegistry) GetAgentUserCount() int
GetAgentUserCount 获取客服用户数 - O(1)
func (*ShardedRegistry) GetAllClients ¶
func (r *ShardedRegistry) GetAllClients() []*models.Client
GetAllClients 获取所有客户端列表 通过 ShardedMap.Range 遍历所有 shard(分片读锁,粒度细)
func (*ShardedRegistry) GetClient ¶
func (r *ShardedRegistry) GetClient(clientID string) (*models.Client, bool)
GetClient 根据 clientID 获取客户端 先查 clientIDToUserID 反向索引定位 shard,再读锁查找
func (*ShardedRegistry) GetClientCount ¶
func (r *ShardedRegistry) GetClientCount() int64
GetClientCount 获取客户端总数(原子读取,零锁开销)
func (*ShardedRegistry) GetClientIPs ¶
func (r *ShardedRegistry) GetClientIPs(ctx context.Context, userID string) []string
GetClientIPs 获取用户所有客户端的IP地址列表(按 ctx 路由信封的 appID+namespace 隔离) 路由信封通过 routing 从 ctx 注入,调用方不需传 appID/namespace 参数 使用 ForEachUserClientFiltered 零拷贝遍历(复用 ClientMatchesEnvelope 单一真相源)
func (*ShardedRegistry) GetClientMetadata ¶
func (r *ShardedRegistry) GetClientMetadata(clientID string, key string) (interface{}, bool)
GetClientMetadata 获取客户端元数据(线程安全)
func (*ShardedRegistry) GetClientStats ¶
func (r *ShardedRegistry) GetClientStats(clientID string) map[string]interface{}
GetClientStats 获取客户端统计信息
func (*ShardedRegistry) GetClientsByDepartmentGrouped ¶
func (r *ShardedRegistry) GetClientsByDepartmentGrouped() map[models.Department][]*models.Client
GetClientsByDepartmentGrouped 按部门分组获取客户端(零拷贝遍历)
func (*ShardedRegistry) GetClientsByStatusGrouped ¶
func (r *ShardedRegistry) GetClientsByStatusGrouped() map[models.UserStatus][]*models.Client
GetClientsByStatusGrouped 按状态分组获取客户端(零拷贝遍历,原子读 Status)
func (*ShardedRegistry) GetClientsByUserID ¶
GetClientsByUserID 根据用户ID获取所有客户端(按 ctx 路由信封的 appID+namespace 隔离) 路由信封通过 routing 从 ctx 注入,调用方不需传 appID/namespace 参数 使用 ForEachUserClientFiltered 持读锁遍历(复用 ClientMatchesEnvelope 单一真相源)
func (*ShardedRegistry) GetClientsByUserTypeGrouped ¶
func (r *ShardedRegistry) GetClientsByUserTypeGrouped() map[models.UserType][]*models.Client
GetClientsByUserTypeGrouped 按用户类型分组获取客户端(零拷贝遍历)
func (*ShardedRegistry) GetClientsCopyForUser ¶
func (r *ShardedRegistry) GetClientsCopyForUser(ctx context.Context, userID, clientID string) []*models.Client
GetClientsCopyForUser 获取用户的客户端列表副本(线程安全,按 ctx 路由信封 appID+namespace 隔离) 如果指定了 clientID,只返回该客户端(O(1) 查找,仍校验 client 的 appID+namespace 信封匹配) 否则返回用户匹配信封的所有客户端 使用 ForEachUserClientFiltered 持读锁遍历,替代 GetUserClients 锁外遍历的数据竞争
func (*ShardedRegistry) GetClientsWithStatus ¶
func (r *ShardedRegistry) GetClientsWithStatus(status models.UserStatus) []*models.Client
GetClientsWithStatus 获取指定状态的所有客户端(委托 FilterClients 零拷贝,原子读 Status)
func (*ShardedRegistry) GetLocalGroupMembers ¶
func (r *ShardedRegistry) GetLocalGroupMembers(appID string, groupIDs []string) []string
GetLocalGroupMembers 返回指定 appID 下多群组的本地在线成员 userID 列表(跨群组去重) 替代 Redis GetMultiGroupMembers 的全量读:各节点仅返回本节点在线成员,扇出范围天然收敛为本地连接 返回列表不含 namespace 维度(群组投递跨 ns 通配),下游 BroadcastToUserIDs 按信封 appID 过滤
func (*ShardedRegistry) GetLocalNamespaceUsers ¶
func (r *ShardedRegistry) GetLocalNamespaceUsers(appID, namespace string) []string
GetLocalNamespaceUsers 返回指定 appID+namespace 下的本地在线用户 userID 列表(设备去重) 命名空间广播(本地投递与跨节点消费侧)经此定点反查后 BroadcastToUserIDs 扇出, 替代 BroadcastToFiltered 对全部连接的 O(N) 全量扫描; namespace 为空时返回 nil:空 ns = 全局广播语义,由全量扫描路径处理(目标集=全部连接,全量即最优), 不允许从本索引取数(索引只按非空 ns 维度建键)
func (*ShardedRegistry) GetLocalUsersByClientType ¶
func (r *ShardedRegistry) GetLocalUsersByClientType(appID string, clientType models.ClientType) ([]string, bool)
GetLocalUsersByClientType 返回指定客户端类型的本地在线用户(设备去重)
func (*ShardedRegistry) GetLocalUsersByDepartment ¶
func (r *ShardedRegistry) GetLocalUsersByDepartment(appID string, dept models.Department) ([]string, bool)
GetLocalUsersByDepartment 返回指定部门的本地在线用户(设备去重)
func (*ShardedRegistry) GetLocalUsersByRole ¶
GetLocalUsersByRole 返回指定角色的本地在线用户(设备去重)
func (*ShardedRegistry) GetLocalUsersByUserType ¶
func (r *ShardedRegistry) GetLocalUsersByUserType(appID string, userType models.UserType) ([]string, bool)
GetLocalUsersByUserType 返回指定用户类型的本地在线用户(设备去重) indexed=false 表示未入索引(空取值),调用方须回退全量扫描
func (*ShardedRegistry) GetMostRecentClient ¶
GetMostRecentClient 获取用户对应的客户端(返回最近活跃的客户端,按 ctx 路由信封 appID+namespace 隔离) 使用 ForEachUserClientFiltered 零拷贝遍历(持读锁),替代 GetUserClients 锁外遍历内部 map 的数据竞争
func (*ShardedRegistry) GetObserverDeviceCount ¶
func (r *ShardedRegistry) GetObserverDeviceCount() int
GetObserverDeviceCount 获取观察者设备总数
func (*ShardedRegistry) GetObserverUserClients ¶
GetObserverUserClients 获取指定观察者的所有客户端
func (*ShardedRegistry) GetObserverUserCount ¶
func (r *ShardedRegistry) GetObserverUserCount() int
GetObserverUserCount 获取观察者用户数(设备去重)- O(1)
func (*ShardedRegistry) GetObserversForMessage ¶
func (r *ShardedRegistry) GetObserversForMessage(namespace string, groupIDs ...string) []*models.Client
func (*ShardedRegistry) GetOnlineUserCountByType ¶
func (r *ShardedRegistry) GetOnlineUserCountByType(userType models.UserType) (int64, error)
GetOnlineUserCountByType 根据用户类型获取在线用户数量 使用 ForEachClient 零拷贝遍历 + 用户去重,替代 GetClientsByUserType 切片拷贝
func (*ShardedRegistry) GetOnlineUserIDs ¶
func (r *ShardedRegistry) GetOnlineUserIDs() []string
GetOnlineUserIDs 获取所有在线用户ID列表 使用 ShardedMap.Keys 直接获取所有 userID
func (*ShardedRegistry) GetOnlineUsersByType ¶
func (r *ShardedRegistry) GetOnlineUsersByType(userType models.UserType) ([]string, error)
GetOnlineUsersByType 按用户类型获取在线用户列表 使用 ForEachClient 零拷贝遍历 + 用户去重,替代 FilterClients 切片拷贝
func (*ShardedRegistry) GetSSEClientCount ¶
func (r *ShardedRegistry) GetSSEClientCount() int64
GetSSEClientCount 获取 SSE 客户端总数(原子读取,零锁开销)
func (*ShardedRegistry) GetSSEUserClients ¶
GetSSEUserClients 获取指定用户的所有 SSE 客户端(用于按 userID 推送) 返回内部 map 引用,调用方应在读锁释放后立即使用或拷贝
func (*ShardedRegistry) GetSSEUserCount ¶
func (r *ShardedRegistry) GetSSEUserCount() int
GetSSEUserCount 获取 SSE 在线用户数
func (*ShardedRegistry) GetSSEUserIDs ¶
func (r *ShardedRegistry) GetSSEUserIDs() []string
GetSSEUserIDs 获取所有 SSE 在线用户ID
func (*ShardedRegistry) GetUserClientCount ¶
func (r *ShardedRegistry) GetUserClientCount(userID string) int
GetUserClientCount 获取用户的客户端数量
func (*ShardedRegistry) GetUserClients ¶
GetUserClients 获取用户的所有客户端 返回的是内部 map 引用(读锁期间获取的快照),调用方不应长时间持有
func (*ShardedRegistry) GetUserClientsCopy ¶
func (r *ShardedRegistry) GetUserClientsCopy() []*models.Client
GetUserClientsCopy 获取每个用户最活跃的客户端副本列表 遍历所有 shard,每个用户取最近活跃的客户端(复用 findMostRecentClient)
func (*ShardedRegistry) GetUserCount ¶
func (r *ShardedRegistry) GetUserCount() int64
GetUserCount 获取在线用户总数(原子读取,零锁开销)
func (*ShardedRegistry) GetUserStatus ¶
func (r *ShardedRegistry) GetUserStatus(ctx context.Context, userID string) models.UserStatus
GetUserStatus 获取用户状态(按 ctx 路由信封的 appID+namespace 隔离) 路由信封通过 routing 从 ctx 注入,调用方不需传 appID/namespace 参数 使用 ForEachUserClientFiltered 零拷贝遍历(复用 ClientMatchesEnvelope 单一真相源)
func (*ShardedRegistry) HasAgent ¶
func (r *ShardedRegistry) HasAgent(userID string) bool
HasAgent 检查用户是否为客服 - O(1)
func (*ShardedRegistry) HasObserver ¶
func (r *ShardedRegistry) HasObserver(userID string) bool
HasObserver 检查用户是否为观察者 - O(1)
func (*ShardedRegistry) HasSSEUser ¶
func (r *ShardedRegistry) HasSSEUser(userID string) bool
HasSSEUser 检查指定用户是否有 SSE 连接
func (*ShardedRegistry) HasUser ¶
func (r *ShardedRegistry) HasUser(userID, appID, namespace string) bool
HasUser 检查指定路由信封(appID+namespace)下某用户是否在线(统一入口,强制 appID+namespace 隔离)
设计原则:appID+namespace 是隔离维度(查询/投递一律走信封),group 是广播维度(与本方法正交) 路由信封来源统一为 ctx(Hub 层 routing.AppIDFromContext/NamespaceFromContext 提取后传入)
复用 ClientMatchesEnvelope 单一真相源:appID 严格匹配 + namespace 空值兼容全局广播 appID 为空时退化为不过滤(兼容无路由 ctx 的边界场景),正常路径入口已归一化为 DefaultAppID 性能:单次读锁内完成存在性检查 + 信封过滤(map miss 即 O(1) 判负,在线场景 2 锁 → 1 锁)
func (*ShardedRegistry) HasUserClient ¶
func (r *ShardedRegistry) HasUserClient(ctx context.Context, userID string) bool
HasUserClient 检查是否存在指定用户ID的客户端(按 ctx 路由信封的 appID+namespace 隔离) 路由信封通过 routing 从 ctx 注入,调用方不需传 appID/namespace 参数
func (*ShardedRegistry) HasUserExactEnvelope ¶
func (r *ShardedRegistry) HasUserExactEnvelope(userID, appID, namespace string) bool
HasUserExactEnvelope 检查指定信封(appID+namespace 原值)下某用户是否在线(分区精确匹配)
与 HasUser 的区别:namespace 空值不做全局放宽——HasUser 的空值语义服务于广播投递范围 (namespace=="" 匹配所有命名空间),本方法服务于分区生命周期判定(系统组成员 join/leave 必须读写同一分区 key,"" 分区不因用户在其他命名空间在线而误判为仍需保留)
func (*ShardedRegistry) ObserverEnabled ¶
func (r *ShardedRegistry) ObserverEnabled() bool
ObserverEnabled 返回观察者模块是否启用(shards 非 nil)
func (*ShardedRegistry) RemoveClient ¶
func (r *ShardedRegistry) RemoveClient(clientID, userID string) *models.Client
RemoveClient 从注册表移除客户端 返回被移除的客户端(nil 表示不存在)
func (*ShardedRegistry) UpdateClientMetadata ¶
func (r *ShardedRegistry) UpdateClientMetadata(clientID string, key string, value interface{}) error
UpdateClientMetadata 更新客户端元数据(线程安全)
type SlowConsumerScanner ¶
type SlowConsumerScanner struct {
// contains filtered or unexported fields
}
SlowConsumerScanner 慢消费者扫描器(周期采样 + 三级治理)
func NewSlowConsumerScanner ¶
func NewSlowConsumerScanner(registry *ShardedRegistry, hook EvictHook, interval time.Duration, logger spi.Logger) *SlowConsumerScanner
NewSlowConsumerScanner 创建慢消费者扫描器 interval ≤ 0 时默认 1s(与原逻辑"准入未配置时 1s"对齐)
func (*SlowConsumerScanner) ScanOnce ¶
func (s *SlowConsumerScanner) ScanOnce()
ScanOnce 单轮扫描(分片并行遍历 + 无锁采样 + 三级治理) 导出供测试与运维接口手动触发;并发手动触发与周期扫描间整轮互斥
func (*SlowConsumerScanner) Start ¶
func (s *SlowConsumerScanner) Start(ctx context.Context)
Start 启动周期扫描(阻塞直至 ctx 取消;Hub Run 时以 go 调用)
周期 = 准入评估周期 × 2(与水位评估同源节拍,避免扫描与评估完全同步造成的 周期性毛刺);扫描本身 O(活跃连接) 原子读遍历
func (*SlowConsumerScanner) Stats ¶
func (s *SlowConsumerScanner) Stats() int
Stats 治理状态统计(观测用:当前处于积压追踪中的连接数)