routing

package
v0.12.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

routing(路由域)

路由信封的构建、注入与提取——appID/namespace/groupIDs 贯穿全链路的唯一载体

这个域干什么

routing 定义路由元数据(appID + namespace + groupIDs)的规范表达与传播: 本地调用链经 Route 链式构建器注入 ctx、*FromContext 系列提取; 跨节点 gRPC 经 metadata headers 传播(x-routing-* 三 header)

隔离层次:AppID(应用) > Namespace(租户) > GroupID(平台) > UserID; 空值归一化在入口层一次做对(Inject / EnsureRouteDefaults),下游不兜底

核心概念

概念 说明
RoutingContext 路由三元组(appID/namespace/groupIDs),一次 ctx 断言全取出
NewRoute / RouteFrom 链式构建器 / 从现有 ctx 继承派生(With* 显式命名每个维度)
Inject / EnsureRouteDefaults 注入 ctx(appID 补默认);严格场景 ns 空补默认(广播场景不补)
*FromContext 系列 提取侧:AppID/Namespace/GroupIDs/FirstGroupID,零分配热路径
NormalizeRoute appID+namespace 归一化(收口调用 constants 原子函数)
InjectToOutgoingMetadata / RestoreFromIncomingMetadata gRPC 跨节点信封的注入与恢复

怎么用

package main

import (
    "context"

    "github.com/kamalyes/go-wsc/routing"
)

// 注入:链式构建器,每个维度显式命名,漏传一目了然
ctx = routing.NewRoute().
    WithAppID("app-1").
    WithNamespace("ns-1").
    WithGroup("group-a").
    Inject(ctx)

// 严格场景兜底(P2P/群组发送;全局广播不要调用——ns 留空=全命名空间)
ctx = routing.EnsureRouteDefaults(ctx)

// 提取:下游随时取,不再兜底
appID := routing.AppIDFromContext(ctx)
ns := routing.NamespaceFromContext(ctx)

端口与依赖

  • 单向依赖 constants:NormalizeAppID/Namespace 原子归一化、MetadataKey 常量、默认值
  • 被 models/group/messaging/cluster 单向依赖;零存储、零 Web 框架依赖

性能与陷阱

  • *FromContext 系列零分配热路径:ctx 无路由就是空串/nil,不做默认值兜底
  • 陷阱:广播场景误调 EnsureRouteDefaults——namespace 空值是"全命名空间"语义,补默认即收窄
  • 陷阱:绕过 Route 直接 context.WithValue——routingCtxKey 不可导出,外部无法伪造

后端绑定

无纯 context 传递与纯函数

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppIDFromContext

func AppIDFromContext(ctx context.Context) string

AppIDFromContext 从 context 提取应用ID(零分配热路径,与 NamespaceFromContext 共享 RoutingFromContext 断言)

appID 为必填维度(无广播语义),此处统一归一化:ctx 无路由或 appID 为空时返回 DefaultAppID 调用方直接使用返回值即可,无需再 NormalizeAppID/NormalizeRoute 二次包装,消除散落归一化

func EnsureRouteDefaults

func EnsureRouteDefaults(ctx context.Context) context.Context

EnsureRouteDefaults 严格场景的路由兜底归一化:appID/namespace 空值补默认值

适用场景:P2P 发送、群组发送等需要完整路由维度的入口(namespace 不允许空) 不适用场景:全局广播(namespace 故意留空表示全命名空间,调用方不应调用此函数)

幂等:已归一化的 ctx 再次调用无副作用(NormalizeRoute 对非空值不覆盖,groupIDs 保留原值) 调用方一行搞定兜底,替代各处散落的 `if appID == "" || namespace == ""` 模板:

ctx = routing.EnsureRouteDefaults(ctx)

func FirstGroupIDFromContext

func FirstGroupIDFromContext(ctx context.Context) string

FirstGroupIDFromContext 从 context 提取首个群组ID(无群组时返回空字符串) 离线消息按单条消息维度存储,群组消息取首个 groupID,点对点消息为空

func GroupIDsFromContext

func GroupIDsFromContext(ctx context.Context) []string

GroupIDsFromContext 从 context 提取群组ID列表

func InjectToOutgoingMetadata

func InjectToOutgoingMetadata(ctx context.Context) context.Context

InjectToOutgoingMetadata 将路由元数据注入 gRPC outgoing metadata groupIDs 以逗号分隔存入单个 metadata 值,避免 repeated header 开销

func IsFanoutSourced

func IsFanoutSourced(ctx context.Context) bool

IsFanoutSourced 判定消息是否来自总线 fanout 通道(扇出已由总线完成,无需再跨节点派发)

func NamespaceFromContext

func NamespaceFromContext(ctx context.Context) string

NamespaceFromContext 从 context 提取命名空间

func NormalizeRoute

func NormalizeRoute(appID, namespace string) (string, string)

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

func RestoreFromIncomingMetadata(ctx context.Context) context.Context

RestoreFromIncomingMetadata 从 gRPC incoming metadata 恢复路由元数据到 ctx 归一化 appID(空值补 DefaultAppID),与入口层策略一致

func WithFanoutSourced

func WithFanoutSourced(ctx context.Context) context.Context

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 NewRoute

func NewRoute() *Route

NewRoute 创建空的路由构建器

func RouteFrom

func RouteFrom(ctx context.Context) *Route

RouteFrom 从现有 context 继承路由维度,便于局部修改后重新注入

适用场景:下游需要改 namespace 但保留 appID/groupIDs 时:

ctx = routing.RouteFrom(ctx).WithNamespace("new-ns").Inject(ctx)

若 ctx 无路由元数据,返回空 Route(后续 With* 再补维度)

func (*Route) Inject

func (r *Route) Inject(ctx context.Context) context.Context

Inject 将构建好的路由元数据注入 context

归一化策略:

  • appID 空补 DefaultAppID(appID 无广播语义,必填,与旧 WithNamespaceGroupIDs 一致)
  • namespace 保持原值(广播场景 namespace="" 表示全命名空间,不归一化;严格场景调用方用 EnsureRouteDefaults 补默认)
  • groupIDs 保持原值(P2P 消息 nil 合法,存储层按需补 DefaultGroupID)

func (*Route) WithAppID

func (r *Route) WithAppID(appID string) *Route

WithAppID 设置应用ID(最上层隔离维度)

func (*Route) WithGroup

func (r *Route) WithGroup(groupID string) *Route

WithGroup 追加单个群组ID(累加,不覆盖已有值;用于逐个添加群组场景)

func (*Route) WithGroupIDs

func (r *Route) WithGroupIDs(groupIDs []string) *Route

WithGroupIDs 设置群组ID列表(覆盖已有值;P2P 消息传 nil 是合法语义)

func (*Route) WithNamespace

func (r *Route) WithNamespace(namespace string) *Route

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 提取路由元数据(一次断言,零分配热路径)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL