Documentation
¶
Overview ¶
- @Author: kamalyes 501893067@qq.com
- @Date: 2026-09-22 16:06:00
- @LastEditors: kamalyes 501893067@qq.com
- @LastEditTime: 2026-09-22 16:06:00
- @FilePath: \go-wsc\routing\propagation.go
- @Description: 路由元数据传播 —— ctx 提取与 gRPC metadata 跨节点传播 *
* 提取侧设计原则:存储/查询层不做默认值归一化,没有就是空串。
归一化统一由入口层(route.go 的 Route.Inject / EnsureRouteDefaults)完成,
避免下游到处兜底导致行为不一致与维护负担。 *
Copyright (c) 2026 by kamalyes, All Rights Reserved.
@Author: kamalyes 501893067@qq.com
@Date: 2026-09-22 16:06:00
@LastEditors: kamalyes 501893067@qq.com
@LastEditTime: 2026-10-26 20:31:08
@FilePath: \go-wsc\routing\route.go
@Description: 路由信封构建与归一化 *
路由元数据(appID + namespace + groupIDs)是路由信封,不属于消息内容:
- 本地调用链:通过 Route 链式构建器注入(NewRoute().WithAppID(...).Inject(ctx)),
通过 propagation.go 的 *FromContext 系列函数提取
- 跨节点 gRPC:通过 propagation.go 的 metadata 传播函数
- 跨节点 PubSub:通过 DistributedMessage.GroupIDs 信封字段携带 *
隔离层次:AppID(应用) > Namespace(租户) > GroupID(平台) > UserID
appID 是最上层隔离维度,但语义上从属于 namespace+group 路由体系:
- 空值在入口层归一化为 DefaultAppID(与 namespace 归一化策略一致)
- 业务代码应通过 routing.NewRoute().WithAppID(...).WithNamespace(...).WithGroupIDs(...).Inject(ctx)
一次性注入全维度,每个维度显式命名,避免位置参数记错或漏传 appID *
设计原则:appid/namespace/group 贯穿整个生命周期都从 ctx 取
- 注入侧:Route 链式构建器(显式命名每个维度,漏传一目了然)
- 提取侧:propagation.go 的 *FromContext 系列函数(零分配热路径,不做默认值兜底)
- 归一化:仅在入口层(Route.Inject / EnsureRouteDefaults)完成,下游不再兜底 *
Copyright (c) 2026 by kamalyes, All Rights Reserved.
Index ¶
- func AppIDFromContext(ctx context.Context) string
- func EnsureRouteDefaults(ctx context.Context) context.Context
- func FirstGroupIDFromContext(ctx context.Context) string
- func GroupIDsFromContext(ctx context.Context) []string
- func InjectToOutgoingMetadata(ctx context.Context) context.Context
- func IsFanoutSourced(ctx context.Context) bool
- func NamespaceFromContext(ctx context.Context) string
- func NormalizeRoute(appID, namespace string) (string, string)
- func RestoreFromIncomingMetadata(ctx context.Context) context.Context
- func WithFanoutSourced(ctx context.Context) context.Context
- type Route
- type RoutingContext
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppIDFromContext ¶
AppIDFromContext 从 context 提取应用ID(零分配热路径,与 NamespaceFromContext 共享 RoutingFromContext 断言)
appID 为必填维度(无广播语义),此处统一归一化:ctx 无路由或 appID 为空时返回 DefaultAppID 调用方直接使用返回值即可,无需再 NormalizeAppID/NormalizeRoute 二次包装,消除散落归一化
func EnsureRouteDefaults ¶
EnsureRouteDefaults 严格场景的路由兜底归一化:appID/namespace 空值补默认值
适用场景:P2P 发送、群组发送等需要完整路由维度的入口(namespace 不允许空) 不适用场景:全局广播(namespace 故意留空表示全命名空间,调用方不应调用此函数)
幂等:已归一化的 ctx 再次调用无副作用(NormalizeRoute 对非空值不覆盖,groupIDs 保留原值) 调用方一行搞定兜底,替代各处散落的 `if appID == "" || namespace == ""` 模板:
ctx = routing.EnsureRouteDefaults(ctx)
func FirstGroupIDFromContext ¶
FirstGroupIDFromContext 从 context 提取首个群组ID(无群组时返回空字符串) 离线消息按单条消息维度存储,群组消息取首个 groupID,点对点消息为空
func GroupIDsFromContext ¶
GroupIDsFromContext 从 context 提取群组ID列表
func InjectToOutgoingMetadata ¶
InjectToOutgoingMetadata 将路由元数据注入 gRPC outgoing metadata groupIDs 以逗号分隔存入单个 metadata 值,避免 repeated header 开销
func IsFanoutSourced ¶
IsFanoutSourced 判定消息是否来自总线 fanout 通道(扇出已由总线完成,无需再跨节点派发)
func NamespaceFromContext ¶
NamespaceFromContext 从 context 提取命名空间
func NormalizeRoute ¶
NormalizeRoute 统一归一化路由隔离维度:appID 空补 constants.DefaultAppID,namespace 空补 constants.DefaultNamespace
设计原则:
- appID 与 namespace 同为入口层必填维度,归一化策略一致(空=默认值,而非空=全局共享)
- groupIDs 不在此归一化(P2P 消息 groupIDs=nil 是合法语义,存储层按需补 DefaultGroupID)
- 严格场景(P2P 发送、群组发送、客户端注册、跨节点恢复)调用此方法一次性归一化两个维度
- 广播场景不应调用此方法(namespace 故意留空表示全命名空间广播,需保留空值)
返回归一化后的 (appID, namespace),调用方按需取用:
appID, ns := routing.NormalizeRoute(appID, ns) // 两个都要(严格场景)
只需归一化 appID 单个维度时,直接用 constants.NormalizeAppID(不要传占位 ns 调本函数再丢弃返回值)
func RestoreFromIncomingMetadata ¶
RestoreFromIncomingMetadata 从 gRPC incoming metadata 恢复路由元数据到 ctx 归一化 appID(空值补 DefaultAppID),与入口层策略一致
func WithFanoutSourced ¶
WithFanoutSourced 标记消息来自事件总线的 fanout 通道(每节点各消费一份)
用途是消除跨节点重复派发:fanout 语义下每个节点都会独立消费到同一条消息, 若各节点再按「源节点」身份向其余节点派发,则 N 节点产生 N×(N−1) 次跨节点调用、 每个连接收到 N 份(本地去重窗是按节点的,拦不住跨节点重复)。 消费侧打上本标记后,submitClusterDispatch 会跳过跨节点派发,只做本地投递。
Types ¶
type Route ¶
type Route struct {
// contains filtered or unexported fields
}
Route 路由构建器,链式设置 appID/namespace/groupIDs 后通过 Inject 注入 context
func RouteFrom ¶
RouteFrom 从现有 context 继承路由维度,便于局部修改后重新注入
适用场景:下游需要改 namespace 但保留 appID/groupIDs 时:
ctx = routing.RouteFrom(ctx).WithNamespace("new-ns").Inject(ctx)
若 ctx 无路由元数据,返回空 Route(后续 With* 再补维度)
func (*Route) Inject ¶
Inject 将构建好的路由元数据注入 context
归一化策略:
- appID 空补 DefaultAppID(appID 无广播语义,必填,与旧 WithNamespaceGroupIDs 一致)
- namespace 保持原值(广播场景 namespace="" 表示全命名空间,不归一化;严格场景调用方用 EnsureRouteDefaults 补默认)
- groupIDs 保持原值(P2P 消息 nil 合法,存储层按需补 DefaultGroupID)
func (*Route) WithGroupIDs ¶
WithGroupIDs 设置群组ID列表(覆盖已有值;P2P 消息传 nil 是合法语义)
func (*Route) WithNamespace ¶
WithNamespace 设置命名空间ID(租户隔离维度;广播场景可省略,表示全命名空间)
type RoutingContext ¶
type RoutingContext struct {
AppID string // 应用ID(最上层隔离维度,Inject 时归一化为 constants.DefaultAppID)
Namespace string // 命名空间ID(严格场景经 NormalizeRoute 归一化为 DefaultNamespace;广播场景保留空值=全命名空间)
GroupIDs []string // 群组ID列表(len==1 单群组,len>1 批量,len==0 无群组)
}
RoutingContext 路由元数据(appID + namespace + groupIDs) 打包存入 context,一次类型断言获取全部,避免多次 ctx.Value + interface 断言
func RoutingFromContext ¶
func RoutingFromContext(ctx context.Context) *RoutingContext
RoutingFromContext 从 context 提取路由元数据(一次断言,零分配热路径)