背景
SharpLink dev 分支当前版本为 0.7.9。现有客户端拓扑模型支持:
- 固定单端点,并保留专门的 fast path。
- 一个静态 endpoint cluster,cluster 内可以有多个对等 endpoint。
- 一个 resolver 驱动的动态 endpoint cluster。
- cluster 内负载均衡、Retry、Endpoint Admission、Circuit Breaker、连接排空和动态拓扑更新。
- Source Generator 生成的程序集 Manifest。
- Client/Server 实例级的运行时程序集注册、替换、排空、注销和 collectible AssemblyLoadContext 释放。
当前缺口是:一个应用进程中的同一个逻辑 RPC 客户端,无法同时连接多个彼此独立的服务 Cluster,并按照 Contract 将调用固定路由到正确的 Cluster。例如:
- orders cluster 提供 IOrderService、IInventoryService。
- payments cluster 提供 IPaymentService。
- 每个 cluster 内部都可能是单节点、静态多节点或 resolver 动态多节点。
- 某些 Contract 程序集在编译时已知并静态注册。
- 某些插件 Contract 程序集在运行时加载,并且必须显式注册到指定 cluster。
本 Issue 定义 0.7.10 的目标、限制、建议 API、内部实现路径、并发和生命周期语义、测试矩阵、验收条件、性能门禁及风险规避策略。实现者应严格按本文逐项核对;如果确实需要偏离,必须先在 PR 描述中解释原因、兼容性和性能影响。
总体结论
新增一个独立的多 Cluster 协调层,而不是把多个 Cluster 压平到现有单个 IEndpointClusterRuntime,也不允许在每次 RPC 调用时查找 cluster。
目标结构:
Application
|
| Get<TContract>():仅在创建 Proxy 时进行一次 Contract -> Cluster 路由
v
SharpLinkMultiClusterClient
|
+-- orders -> 一个现有 SharpLinkClient -> 单端点或 N 个 endpoint
|
+-- payments -> 一个现有 SharpLinkClient -> 单端点或 N 个 endpoint
|
+-- plugins -> 一个现有 SharpLinkClient -> 单端点或 N 个 endpoint
每个 Cluster Slot 内部复用现有 SharpLinkClient。生成 Proxy 时,将目标 child client 或其动态模块 channel 直接传给 ProxyFactory。Proxy 创建后,RPC 热路径不得再次访问多 Cluster 路由表。
术语
为了避免与现有 UseCluster 混淆,本文使用以下术语:
- Endpoint cluster:现有 0.7.5+ 的概念;一个 SharpLinkClient 内部的一组对等 endpoint。
- Multi-cluster client:0.7.10 新增的协调器;拥有多个相互隔离的 SharpLinkClient。
- Cluster Slot:Multi-cluster client 内部的一个命名成员;每个 Slot 对应一个现有 SharpLinkClient。
- Contract route:ContractType/ContractId 到唯一 Cluster Slot 的映射。
- Static contract assembly:构建 Multi-cluster client 前已经加载、具有 Source Generator Manifest,并由编译期 route manifest 指定 Cluster 的程序集。
- Dynamic contract assembly:Build 后加载,并通过 RegisterAssembly(cluster, assembly) 注册到指定 Cluster 的程序集。
- Routed assembly:拥有至少一个 Contract descriptor,并且已分配到唯一 Cluster 的程序集。
- Dependency-only assembly:Manifest 不拥有 Contract,只提供 Codec 或其他生成依赖的程序集。
必须实现的目标
G1. 多 Cluster
一个 Multi-cluster client 必须能够配置至少一个 Cluster Slot。每个 Slot 必须可以使用现有 SharpClientBuilder 支持的任一拓扑:
- fixed transport;
- 单 endpoint;
- 静态多 endpoint;
- resolver 动态 endpoint。
Slot 内部的 Retry、Load Balancing、Admission、Circuit Breaker、连接池、认证、拦截器、序列化和协议配置继续使用现有 SharpClientBuilder 语义。
G2. Contract 唯一路由
每个已暴露 Contract 必须且只能映射到一个 Cluster Slot。
client.Get() 必须:
- 从原子 Contract route snapshot 中按 typeof(TContract) 查找 Cluster Slot;
- 只在 Get 阶段完成路由;
- 调用目标 child client 的 Get() 创建 Proxy;
- 返回直接绑定目标 IRpcChannel 的生成 Proxy。
调用 Proxy 方法之后,不允许:
- 再按 Contract 查 Cluster;
- 查 cluster 名称;
- 解析字符串;
- 创建路由上下文;
- 给每个请求增加 cluster 字段;
- 在 ClientConnection/PendingCall/请求帧中保存 cluster 名称。
G3. 编译期静态程序集路由
必须提供 Source Generator 可验证的静态程序集到 Cluster 映射。推荐 API:
[assembly: SharpLinkClusterContractAssembly(
"orders",
typeof(OrderContractsAssemblyMarker))]
[assembly: SharpLinkClusterContractAssembly(
"payments",
typeof(PaymentContractsAssemblyMarker))]
语义:
- assemblyMarker 只用于定位其所在程序集。
- 目标程序集 Manifest 中的全部 Contracts 作为一个不可拆分单元分配给指定 Cluster。
- 同一 Contract-owning assembly 不得静态分配给多个 Cluster。
- 同一映射重复声明到同一个 Cluster 可以去重,但 Generator 应给出不阻塞编译的提示或保持静默;不得生成重复 route。
- 如果同一程序集或同一 Contract 被分配到不同 Cluster,必须产生编译期错误。
- Attribute 应放在应用/Host 程序集,不能要求可复用 Contract 包把部署 cluster 名写死在包内。
- Generator 必须输出独立的 generated cluster route manifest 和 ModuleInitializer 注册代码。
- 不要修改现有 ISharpLinkGeneratedAssemblyManifest 的 API 版本;0.7.x 不应因为新增 Cluster 路由而要求所有旧 Contract 程序集重新生成。
- 建议新增独立的 ISharpLinkGeneratedClusterRouteManifest 和弱引用 catalog。
必须支持 NativeAOT 的纯静态路径,不允许依赖反射扫描程序集内容。
G4. 指定 Cluster 的动态程序集注册
必须提供显式 Cluster 参数:
SharpLinkAssemblyRegistrationResult result =
client.RegisterAssembly("payments", pluginAssembly);
SharpLinkAssemblyUnregisterResult drained =
await client.UnregisterAssemblyAsync(
"payments",
pluginAssembly,
TimeSpan.FromSeconds(10),
cancellationToken);
SharpLinkAssemblyReplacementResult replaced =
await client.ReplaceAssemblyAsync(
"payments",
oldAssembly,
newAssembly,
TimeSpan.FromSeconds(10),
cancellationToken);
要求:
- 复用现有 SharpLinkClient 的 Manifest 加载、依赖验证、冲突验证、模块租约、排空、定点取消和 ALC 释放机制。
- 不得增加第二套 RPC Proxy/Codec/动态模块注册系统。
- 注册必须是事务式的:失败后不能留下部分 route、部分 codec 或无法访问的 child registration。
- Contract-owning dynamic assembly 只能路由到一个 Cluster。
- Dependency-only assembly 可以注册到多个 Cluster,但必须以 (ClusterKey, Assembly object) 作为实例级所有权键,并且只有所有相关 Cluster 都注销后,外部 ALC 才可能被释放。
- 动态 Contract assembly 的依赖必须已经注册在同一目标 Cluster;不得从另一个 Cluster 隐式借用 Codec 或模块。
- NativeAOT 下继续返回现有 PlatformNotSupported 结构化结果;不得增加反射 fallback。
G5. 隔离性
Cluster 之间必须隔离:
- endpoint selection;
- Retry exclusion mask;
- Circuit Breaker generation;
- Admission policy;
- Resolver 生命周期;
- 连接池和连接状态;
- PendingCall;
- Streaming;
- Health Check;
- 认证与 transport factory 所有权。
一个 Cluster 的断连、Resolver 异常、Breaker Open、Retry 或动态拓扑替换不得将调用路由到另一个 Cluster,也不得修改其他 Cluster 的 Ready/Draining 状态。
G6. 兼容性和零禁用开销
不使用 Multi-cluster API 时:
- SharpClientBuilder.Build() 必须继续构建现有 SharpLinkClient。
- 现有 fixed endpoint fast path 不得新增分支、字段读取、字典查询、分配、锁或后台任务。
- Protocol v2 wire format 和 handshake capability 不得变化。
- 现有 ISharpLinkClient API 和行为不得发生破坏性变化。
- 现有 0.7.9 Client/Server 必须保持互操作。
使用 Multi-cluster API 时:
- 每次 Proxy 创建允许一次 route snapshot 查询。
- Proxy 创建完成后的每次 RPC 调用必须与直接使用对应 child SharpLinkClient 的路径相同。
- 不得在请求 Header、Metadata 或业务 Payload 中发送 cluster 名称。
- 不得为了路由创建每调用 wrapper、delegate、closure 或 AsyncLocal。
明确的非目标
以下内容不属于 0.7.10;实现者不得顺便加入:
- 同一 Contract 同时部署到多个 Cluster 后按调用选择 Cluster。
- client.Get(cluster) 绕过唯一 Contract 路由。
- 跨 Cluster failover。
- 跨 Cluster Retry。
- Cluster federation。
- Contract 在运行中从一个 Cluster 无缝迁移到另一个 Cluster。
- 在每个 RPC 帧中携带 cluster ID。
- 新增协议版本或 handshake cluster fingerprint。
- Server 在同一个 listener 上托管多个逻辑 Cluster。
- 自动发现 Consul/Nacos/Etcd 中的“Cluster 列表”;现有 Resolver 仍只管理一个 Slot 内的 endpoint。
- 动态拆分一个 Contract-owning assembly,使其中不同 Contract 路由到不同 Cluster。
- 动态 Replace 时改变 ContractId 集合。
- 懒连接、按首次 Get 自动启动 Slot;0.7.10 所有配置 Slot 都由 ConnectAsync 管理。
- 修改现有 Retry、Breaker 或 endpoint selection 算法。
如果未来需要上述能力,应单独设计,不得为了“预留”而给当前每调用热路径增加间接层。
公共 API 参考
名称可以在实现前进行一次统一命名调整,但语义和边界必须保持。PR 中必须列出最终 API 对照。
Cluster Key
建议新增不可变值类型,内部和公共 API 不直接散布裸 string:
public readonly record struct SharpLinkClusterKey
{
public SharpLinkClusterKey(string value);
public string Value { get; }
public override string ToString();
}
验证规则:
- 非 null、非空。
- 长度 1 到 64。
- 第一个字符必须为 ASCII 字母或数字。
- 后续字符仅允许 ASCII 字母、数字、点、下划线和短横线。
- 使用 StringComparer.Ordinal,区分大小写。
- 不得自动 Trim、转小写或进行 Unicode normalization。
- 非法值抛 ArgumentException/ArgumentOutOfRangeException。
- Generator 和 Runtime 必须使用完全相同的验证逻辑;验证逻辑应放在 Abstractions 可复用位置,避免两份实现漂移。
可以提供 string 到 SharpLinkClusterKey 的便利转换,但内部快照键必须是 SharpLinkClusterKey。
Builder
public sealed class SharpLinkMultiClusterClientBuilder
{
public static SharpLinkMultiClusterClientBuilder Create();
public SharpLinkMultiClusterClientBuilder Configure(
Action<SharpLinkMultiClusterOptions> configure);
public SharpLinkMultiClusterClientBuilder AddCluster(
SharpLinkClusterKey cluster,
Action<SharpClientBuilder> configure);
public ISharpLinkMultiClusterClient Build();
}
参考用法:
var client = SharpLinkMultiClusterClientBuilder.Create()
.Configure(options =>
{
options.MaxClusters = 16;
options.MaxTotalConfiguredConnections = 64;
options.MaxConcurrentClusterConnects = 4;
})
.AddCluster("orders", child => child
.UseEndpoints(orderEndpoints, CreateOrderTransport)
.UseCluster(options =>
{
options.MaxConnections = 8;
options.MaxConnectionsPerEndpoint = 2;
})
.UseRetry())
.AddCluster("payments", child => child
.UseEndpointResolver(paymentResolver, CreatePaymentTransport)
.UseCluster(options =>
{
options.MaxConnections = 4;
})
.UseCircuitBreaker(options =>
{
options.MinimumThroughput = 20;
}))
.Build();
await client.ConnectAsync();
IOrderService orders = client.Get<IOrderService>();
IPaymentService payments = client.Get<IPaymentService>();
AddCluster 的 configure 参数可以直接使用现有 SharpClientBuilder,但 Multi-cluster builder 必须通过 internal BuildCore/BuildContext 将过滤后的静态 Manifest 集合交给 child。不得先 Build 普通 child,再让每个 child 自动快照全部全局 Manifest。
Multi-cluster interface
建议:
public interface ISharpLinkMultiClusterClient : IAsyncDisposable
{
SharpLinkMultiClusterState State { get; }
ValueTask ConnectAsync(CancellationToken cancellationToken = default);
ValueTask StopAsync(CancellationToken cancellationToken = default);
TContract Get<TContract>() where TContract : IService;
SharpLinkConnectionState GetClusterState(SharpLinkClusterKey cluster);
ValueTask<SharpLinkHealthCheckResult> CheckHealthAsync(
SharpLinkClusterKey cluster,
CancellationToken cancellationToken = default);
SharpLinkAssemblyRegistrationResult RegisterAssembly(
SharpLinkClusterKey cluster,
Assembly assembly);
ValueTask<SharpLinkAssemblyUnregisterResult> UnregisterAssemblyAsync(
SharpLinkClusterKey cluster,
Assembly assembly,
TimeSpan gracefulTimeout,
CancellationToken cancellationToken = default);
ValueTask<SharpLinkAssemblyReplacementResult> ReplaceAssemblyAsync(
SharpLinkClusterKey cluster,
Assembly oldAssembly,
Assembly newAssembly,
TimeSpan gracefulTimeout,
CancellationToken cancellationToken = default);
}
ISharpLinkMultiClusterClient 不应继承 ISharpLinkClient,因为 ISharpLinkClient.CheckHealthAsync() 和 RegisterAssembly(Assembly) 在多 Cluster 下含义不明确。
Multi-cluster state
建议新增:
public enum SharpLinkMultiClusterState
{
Created,
Connecting,
Ready,
Degraded,
Draining,
Stopped,
Faulted
}
最低语义要求:
- Created:尚未开始共享 Connect。
- Connecting:共享 Connect 已开始但尚未完成。
- Ready:所有配置 Cluster 均至少达到各自现有 ConnectAsync 成功条件。
- Degraded:Connect 曾成功,但之后至少一个 Cluster 不可用,同时至少一个 Cluster 仍可用。
- Draining:Stop 已开始。
- Stopped:所有 child、后台任务、resolver、transport factory 和动态模块清理流程均已完成。
- Faulted:首次 Connect 失败并已完成部分启动清理,或出现不能继续安全使用的协调器级错误。
Cluster 自身的详细状态由 GetClusterState 返回。不得用协调器 State 替代 child 状态。
Options 和资源上限
建议:
public sealed class SharpLinkMultiClusterOptions
{
public int MaxClusters { get; set; } = 16;
public int MaxTotalConfiguredConnections { get; set; } = 64;
public int MaxConcurrentClusterConnects { get; set; } = 4;
}
要求:
- MaxClusters 硬上限不超过 256。
- MaxConcurrentClusterConnects 范围 1 到 64。
- Build 时对所有 child 的配置 MaxConnections 求和,并验证不超过 MaxTotalConfiguredConnections。
- 如果某种 child 模式没有显式 MaxConnections,必须使用其最终冻结后的默认值参与计算。
- AddCluster 不得覆盖同名 Cluster。
- 没有任何 Cluster 时 Build 失败。
- 静态 route 引用不存在 Cluster 时 Build 失败。
- 允许一个 Slot 没有静态 Contract,但必须显式标记为允许动态 Contract;推荐在 AddCluster 增加 options 或单独的 AllowDynamicContracts 配置。默认空 Slot 构建失败,防止拼写错误造成空路由。
静态 route manifest 设计要求
建议新增以下独立模型,不修改现有 generated assembly manifest API:
public sealed record SharpLinkGeneratedClusterAssemblyRoute(
SharpLinkClusterKey Cluster,
Assembly ContractAssembly,
string ContractAssemblyIdentity);
public interface ISharpLinkGeneratedClusterRouteManifest
{
Assembly OwnerAssembly { get; }
IReadOnlyList<SharpLinkGeneratedClusterAssemblyRoute> Routes { get; }
}
public static class SharpLinkGeneratedClusterRouteCatalog
{
public static void Register(ISharpLinkGeneratedClusterRouteManifest manifest);
public static IReadOnlyList<ISharpLinkGeneratedClusterRouteManifest> CreateSnapshot();
}
要求:
- Catalog 使用弱引用和 collectible ALC Unloading 清理,行为参考现有 SharpLinkGeneratedAssemblyCatalog。
- CreateSnapshot 返回强引用点时快照,但 Stop/Dispose 后协调器必须释放自己的强引用。
- Route manifest 必须由 Host/Application 程序集拥有。
- ContractAssemblyIdentity 必须用于结构化诊断;错误结果和异常文本不得长期强引用动态 Assembly/Type。
- Generator 输出必须确定性:相同输入生成字节稳定、顺序稳定的 route descriptor。
- Generator 必须验证 marker assembly 具有兼容的 SharpLink generated manifest。
- 静态路由只包含被 route attribute 明确引用的程序集;进程中其他已加载但未声明的 Contract manifest 不应自动暴露,也不应导致 Multi-cluster Build 失败。
- 一个 routed Contract assembly 的全部 Contracts、相关 generated Codec 和声明依赖必须放入同一个 child build manifest 集合。
- 依赖 Manifest 若被多个 Slot 使用,可以在每个 child 的实例级 runtime context 中各自发布;不得引入进程级可变 Codec 状态。
内部数据结构建议
协调器至少应有:
private FrozenDictionary<Type, ClusterRouteRegistration> _routes;
private FrozenDictionary<SharpLinkClusterKey, ClusterSlot> _clusters;
private readonly Lock _registryGate;
private long _registryGeneration;
ClusterRouteRegistration 至少包含:
- Contract Type。
- ContractId。
- Fingerprint。
- 目标 ClusterSlot。
- 静态或动态 Owner 信息。
- 动态模块状态或足以定位 child registration 的信息。
ClusterSlot 至少包含:
- SharpLinkClusterKey。
- 内部 ISharpLinkClient/SharpLinkClient。
- 冻结后的 child 配置摘要。
- 是否允许动态 Contract。
- 生命周期状态访问。
- child 的静态 Manifest 集合。
关键规则:
- _routes 的读取必须 Volatile.Read + FrozenDictionary.TryGetValue。
- Get 不获取 writer lock。
- 路由更新先构造完整候选快照,再 Volatile.Write 一次发布。
- 不允许原地修改已发布 Dictionary、数组或 descriptor。
- Cluster 集合 Build 后不动态增加/删除;0.7.10 只允许动态增加/删除指定 Slot 内的程序集路由。
- 不要给每个调用创建 ClusterRouteRegistration 副本。
构建流程
Build 必须按以下顺序执行:
- 冻结并验证 Multi-cluster options。
- 冻结所有 AddCluster 配置,检查 ClusterKey 唯一性。
- 为每个 child 取得最终连接上限,检查全局预算。
- 从 SharpLinkGeneratedClusterRouteCatalog 取得静态 route manifest 快照。
- 展开 assembly marker 路由,并定位对应 ISharpLinkGeneratedAssemblyManifest。
- 验证目标 Cluster 存在。
- 验证同一 Contract-owning assembly 只路由到一个 Cluster。
- 验证跨所有 Slot 的 ContractType 和 ContractId 唯一。
- 验证 Fingerprint/Manifest API/Protocol 兼容性。
- 计算每个 Slot 所需的静态 Manifest 闭包,包括 Codec/依赖 Manifest。
- 检查依赖闭包没有缺失或跨 Slot 隐式借用。
- 通过 internal SharpClientBuilder.BuildCore(buildContext) 构建每个 child。
- child buildContext 只包含该 Slot 的静态 Manifest;普通 SharpClientBuilder.Build() 仍使用现有全量快照行为。
- 任一 child Build 失败时,以逆序释放之前已创建 child/transport factory;聚合清理异常,不得泄漏。
- 构造 Cluster 和 Contract FrozenDictionary。
- 返回协调器。Build 不连接网络。
必须避免直接修改 SharpLinkClient 的每调用入口。Manifest 过滤应通过构造/BuildContext 传入,而不是在 Invoke 时检查。
Connect、运行和 Stop 语义
Connect
- ConnectAsync 是共享、幂等操作;并发调用等待同一个 Task。
- 使用 MaxConcurrentClusterConnects 有界并发启动 child。
- 0.7.10 所有配置 Slot 默认都是 required。
- 只有所有 child ConnectAsync 成功,协调器 ConnectAsync 才成功并进入 Ready。
- 任一 child 首次连接失败:
- 停止继续调度尚未开始的 child;
- 对已经成功或正在启动的 child 执行 StopAsync;
- 等待全部清理;
- 聚合保留原始连接错误和清理错误;
- 协调器进入 Faulted;
- 不留下可被 Get 后实际调用的部分启动对象。
- 调用者 cancellation 只取消调用者等待还是取消共享首次 Connect,应与现有 SharpLinkClient.ConnectAsync 语义保持一致,并在文档中明确;不得出现一位调用者取消导致另一位调用者拿到半初始化状态。
- 首次 Ready 后,某个 child 进入 Reconnecting/Faulted:
- 协调器进入 Degraded;
- 其他 child 保持可调用;
- Contract 仍固定到原 Slot;
- 不进行跨 Cluster fallback。
- 所有 child 再次 Ready 后恢复 Ready。
Get
- Created/Connecting 状态是否允许 Get 可以沿用现有 Client:允许创建 Proxy,但调用时由 child 的 readiness/WaitForReady 语义决定。
- Draining/Stopped/Faulted 状态不得返回新动态模块 Proxy。
- 未映射 Contract 抛 InvalidOperationException,消息包含 Contract full name,但不得泄漏 endpoint 地址。
- 动态 route 为 Draining 时,Get 或后续调用必须得到稳定的 Unavailable/InvalidObjectState 语义,不能路由到别的 Slot。
Stop/Dispose
- StopAsync 是共享、幂等操作。
- 先原子进入 Draining,拒绝新的动态注册。
- 对所有 child 发起 Stop;允许有界并行,但不能因为一个 child 抛异常就跳过其他 child。
- 等待 child background task、resolver、connection、transport factory、动态 unregister cleanup。
- 清空协调器强引用 route snapshot、cluster snapshot 和 route manifest snapshot。
- 所有资源清理后进入 Stopped。
- 一个异常直接重抛,多个异常 AggregateException。
- DisposeAsync 调用 StopAsync。
- 绝不在持有 coordinator writer lock 时 await child Stop/Unregister。
Lock 顺序
必须书面固定并在代码注释中记录:
- Coordinator registry/lifecycle gate。
- Child 自己的 registry/state gate。
禁止:
- 持有 child gate 后回调 coordinator。
- 持有 coordinator gate 时 await。
- 在任何 gate 内调用用户提供的 resolver、selector、admission、retry、interceptor、authenticator、transport factory 或 ServiceProvider 代码。
- 在 route snapshot 读路径加锁。
动态注册事务
RegisterAssembly(cluster, assembly) 至少按以下步骤:
- 参数和 ClusterKey 验证。
- 确认 Slot 存在且允许动态 Contract。
- 确认协调器不是 Draining/Stopped/Faulted。
- 使用现有 SharpLinkAssemblyManifestLoader 加载 Manifest。
- 计算 incoming ContractType/ContractId/Fingerprint。
- 在 coordinator writer gate 下,基于 _registryGeneration 和当前 route snapshot 检查:
- 同 Assembly 是否已在目标 Slot 注册;
- Contract-owning Assembly 是否已注册到其他 Slot;
- ContractType/ContractId 是否与静态或动态 route 冲突;
- 依赖是否在目标 child 注册;
- 容量是否超限。
- 调用目标 child 的现有事务式 RegisterAssembly。
- child 失败时直接返回,coordinator route 不变化。
- child 成功后构造新的 Frozen route snapshot 并单次发布。
- 增加 generation,返回 Success。
- 如果 child 成功而 coordinator 发布前发生可恢复异常,必须立即对 child 启动零等待注销并返回结构化失败;PR 必须包含这个 rollback 的测试。
- OOM/StackOverflow 不需要伪装成结构化结果,但不得捕获后继续运行在未知状态。
为了降低复杂度,可以序列化 coordinator 动态 writer。Get 和 RPC 调用保持 lock-free。
并发竞态要求
以下竞态必须只有一个确定结果:
- 两个线程将同一 Contract 注册到不同 Cluster:一个成功,一个稳定 Conflict;不能都成功。
- Register 与 Stop:要么 Register 完整提交并由 Stop 清理,要么 Register 返回 InvalidObjectState;不能 orphan。
- Register 与 Unregister 同一 (Cluster, Assembly):遵循现有 child 重入/共享 operation 语义。
- Replace 与 Stop:publication 和 Draining 的先后必须线性化。
- Get 与 route publication:只能看到旧完整快照或新完整快照。
- Get 与 Unregister:已有 Proxy 按现有模块租约排空;新 Get 不得得到指向已 Released 模块的 Proxy。
Unregister 和 Replace 限制
Unregister
- 参数必须包含 Cluster,且必须与实际 owner Slot 匹配。
- 开始注销时 route 进入 Draining,不迁移到其他 Slot。
- 已有调用和流继续使用现有 child 模块租约排空。
- gracefulTimeout 后复用现有定点取消。
- 调用者 cancellation 只停止等待,不回滚已开始的注销。
- route 在注销完成/进入不可再接受新调用的状态后从 coordinator snapshot 删除。
- ReferencesReleased=false 时不能重新暴露 route;后台释放完成后清理强引用。
- 错误结果不得长期保存 Assembly/Type 强引用。
Replace
0.7.10 限制:
- old 和 new 必须属于同一 Cluster。
- new Manifest 的 ContractId 集合必须与 old 完全一致。
- 不允许 Replace 借机新增、删除 Contract 或改变 Cluster。
- Fingerprint/Schema 是否允许变化沿用现有 ReplaceAssemblyAsync 的兼容验证。
- child publication 与 coordinator 所观察到的 route descriptor 更新必须作为一个逻辑事务。
- 已取得的旧 Proxy 按现有模块 drain 语义结束;新的 Get 使用新 descriptor。
- 若需要改变 Contract 集合,应先 Unregister,再 Register;不提供零间隙迁移保证。
Hosting 集成
必须新增与现有 Hosting 风格一致的入口,建议:
services.AddSharpLinkMultiClusterClient(builder =>
{
builder
.AddCluster("orders", child => child.UseTcp(...))
.AddCluster("payments", child => child.UseEndpointResolver(...));
});
以及:
public interface ISharpLinkMultiClusterClientAccessor
{
ValueTask<ISharpLinkMultiClusterClient> GetClientAsync(
CancellationToken cancellationToken = default);
}
要求:
- HostedService 在全部 required child 连接成功后发布 coordinator。
- Start 失败时停止所有已启动 child,并让 accessor 等待者收到原始错误。
- Stop 后 accessor 返回 unavailable。
- 不向 DI 暴露内部 child ISharpLinkClient,避免调用方绕过 Contract route。
- 同一个 IServiceCollection 默认只注册一个 multi-cluster coordinator;重复注册行为与现有 Hosting API 保持一致并有测试。
- Anonymous Pipe 等只能用于单连接/特殊所有权的 transport,继续服从现有 child Builder 验证,不为 Multi-cluster 添加特殊绕过。
可观测性要求
必须做到:
- 日志 scope 可以包含 cluster.name。
- Activity 在 listener 启用时可以增加 sharplink.cluster.name。
- 默认每调用 Metrics 不得增加 cluster.name、endpoint ID、地址、authority 或 transport 名称标签。
- 可以增加低基数的 coordinator gauge/counter,例如 configured clusters、ready clusters、degraded transitions,但不得每个 Cluster 创建永久 Timer。
- 不启用 Multi-cluster 时不得增加 listener 检查或 metric 调用。
- 动态注册、冲突、启动失败和 Stop 清理错误日志必须包含 ClusterKey 和程序集字符串 identity。
- 不得因日志/错误对象持有 collectible Assembly、Type、Manifest 或 ALC。
建议文件和实现阶段
阶段 1:Abstractions 与 Generator
建议新增/修改:
- src/SharpLink.Abstractions/SharpLinkClusterKey.cs
- src/SharpLink.Abstractions/ISharpLinkMultiClusterClient.cs
- src/SharpLink.Abstractions/SharpLinkMultiClusterState.cs
- src/SharpLink.Abstractions/SharpLinkGeneratedClusterRouteManifest.cs
- src/SharpLink.Sdk/SharpLinkClusterContractAssemblyAttribute.cs
- src/SharpLink.Generator/RpcGenerator.ClusterRouteAnalysis.cs
- src/SharpLink.Generator/RpcGenerator.ClusterRouteEmitter.cs
- AnalyzerReleases.Unshipped.md
先完成 Attribute、诊断、确定性生成和 catalog 测试。
阶段 2:Child Manifest 过滤
重构 SharpClientBuilder:
- 保持 public Build() 行为不变。
- 提取 internal BuildCore。
- 增加 internal build context,可传入精确 static manifest snapshot。
- SharpLinkClient 不再在字段初始化器中无条件抓取全局 Manifest;由构造参数决定,null 才代表普通现有行为。
- 验证普通单 Client 的现有测试和性能完全不变。
这一阶段不得引入 Multi-cluster per-call 分支。
阶段 3:Coordinator 与静态路由
建议新增:
- src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs
- src/SharpLink.Client/SharpLinkMultiClusterOptions.cs
- src/SharpLink.Client/SharpLinkMultiClusterClient.cs
- src/SharpLink.Client/SharpLinkMultiClusterClient.Lifecycle.cs
- src/SharpLink.Client/SharpLinkMultiClusterClient.Assemblies.cs
完成 Build、Connect、Get、Health、Stop 和静态 route。
阶段 4:动态注册
实现 Register/Unregister/Replace、writer serialization、rollback、依赖验证和 ALC 清理。
阶段 5:Hosting、文档和性能
- Hosting accessor/HostedService。
- README 最小示例。
- doc/architecture-0.7.10.md。
- doc/migration-0.7.10.md。
- doc/performance-0.7.10.md。
- CHANGELOG 0.7.10。
- PackageSmoke、NativeAOT、LoadTest/Benchmark 入口。
每个阶段都应保持可构建、测试通过;不要把全部变化堆在一个不可审查提交中。
必须完成的测试
下面是最低测试矩阵,不是可选建议。
A. Generator tests
B. Builder/unit tests
C. Contract routing tests
D. Lifecycle integration tests
至少建立两个真实 Server:
E. Dynamic assembly tests
F. Hosting tests
G. 协议和兼容测试
H. Chaos/压力测试
验收条件
功能验收必须全部满足:
性能基准门禁
性能门禁必须以实现分支从 dev 分叉时的 0.7.9 commit 作为 baseline,并在报告中记录精确 SHA、机器、OS、SDK、CPU governor/电源状态和命令。禁止只给单轮最好结果。
测量方法
- Release 构建,无 debugger/profiler。
- baseline/candidate 交替 A/B,至少 5 轮。
- 相同机器、相同进程模型、相同 concurrency、相同 payload。
- 预热后采样。
- 输出机器可读 JSON 和摘要。
- 报告中给出每轮原始 QPS、P50、P99、错误数、CPU、进程分配及 BenchmarkDotNet B/op。
- 以五轮中位数作为主要判定,同时检查是否存在持续方向性退化。
- 所有性能测试必须零 unexpected failure。
Gate P1:未启用 Multi-cluster 的零回归
比较:
- baseline 0.7.9 普通 SharpClientBuilder;
- candidate 0.7.10 普通 SharpClientBuilder。
至少覆盖:
- fixed TCP Unary 小 payload;
- Unix Domain Socket;
- Named Pipe(支持平台);
- Shared Memory(支持平台);
- c1、c8、c32、c128;
- Rpc_Add BenchmarkDotNet 精确分配。
硬门禁:
- Candidate QPS 中位数不得低于 baseline 的 99%。
- Candidate P99 中位数不得高于 baseline 的 105%。
- BenchmarkDotNet B/op 必须完全不增加;允许测量显示的自然对齐差异必须用反汇编/Allocation 证据解释并经人工接受,不能自行放宽。
- 每调用进程级 allocation 不得增加。
- fixed fast path 不得增加 coordinator 类型实例、后台 Task、Timer、route lookup 或 listener check。
- 如出现超过门禁的退化,必须回退实现,不接受以“功能默认关闭”解释,因为目标是禁用时零热路径影响。
Gate P2:启用 Multi-cluster 后的调用路径
比较:
- 直接使用一个配置完全相同的 child SharpLinkClient;
- 通过 Multi-cluster client Get() 取得 Proxy 后调用同一 child。
测量阶段必须在 Proxy 已经取得后开始,排除一次性 Get 成本。
硬门禁:
- QPS 中位数不得低于直接 child 的 99%。
- P99 中位数不得高于直接 child 的 105%。
- B/op 必须与直接 child 完全一致。
- 采样/测试 hook 必须证明 RPC 次数增加时 coordinator route lookup 次数不增加。
- 不得出现 per-call string/hash lookup、额外 delegate、wrapper allocation 或 AsyncLocal。
Gate P3:Cluster 隔离压力
场景:
- Cluster A 持续 endpoint resolver 更新或 register/unregister。
- Cluster B 持续调用静态 Unary。
- 对照组为 Cluster B 单独运行相同调用。
门禁:
- Cluster B QPS 中位数不得低于对照组的 97%。
- Cluster B P99 中位数不得高于对照组的 105%。
- Cluster A writer 活动不得让 Cluster B 调用进入 coordinator lock。
- 零 unexpected failure。
- 动态 writer 停止后稳态 allocation 回到基线。
Gate P4:启动和资源
这不是每调用热路径,但必须记录:
- 2、8、16 个 Cluster 的 Build/Connect 时间。
- configured connections 与实际连接数。
- coordinator 额外托管内存。
- Stop 总时间和资源归零时间。
门禁:
- 连接数不得超过冻结配置总预算。
- 每个 Cluster 不得新增除现有 child 所需之外的永久 Timer。
- Stop 后所有 gauges 回零,resolver/transport factory 恰好释放一次。
- 16 Cluster 的 coordinator 本身不得创建 O(cluster × contract × call) 的运行期结构;静态内存应近似 O(cluster + routed contracts)。
性能报告必须提交到 doc/performance-0.7.10.md;大型原始 artifacts 可以继续保存在忽略目录,但 PR/CI 必须保留足够的可复现命令和摘要。
可能风险与规避路线
R1. 每个 child 自动看到全部全局 Manifest
风险:Contract 边界失效、重复字典、Codec 错误共享、动态冲突。
规避:
- 先实现 internal BuildContext 精确传入 static Manifest 闭包。
- 普通 Build 传 null 保持旧行为。
- Multi-cluster child 不允许自行调用全局 CreateSnapshot。
- 增加测试直接检查 child route/codec snapshot。
R2. 在每次调用时路由 Cluster
风险:吞吐下降、分配增加、动态 remap 语义复杂。
规避:
- 仅 Get() 查 route。
- 复用 ProxyFactory(IRpcChannel)。
- Proxy 直接捕获 child channel。
- 禁止运行时跨 Cluster remap。
- 用 lookup counter 和 BenchmarkDotNet 门禁。
R3. 动态模块/ALC 泄漏
风险:coordinator route、错误结果、日志、Proxy 或 Codec snapshot 强引用 collectible Assembly。
规避:
- route 注销后原子替换 snapshot 并释放旧 snapshot。
- 错误结果只保存字符串 identity。
- 复用现有 module lease/drain。
- dependency-only 多 Slot 注册使用明确 ref ownership。
- Stop 清空全部强引用。
- 强制 GC 的 collectible ALC integration test。
R4. 注册成功但 route 发布失败
风险:child 内存在不可访问模块。
规避:
- writer serialization。
- 发布前完整构造候选。
- child 成功后 publication 异常执行零等待 rollback unregister。
- fault injection test。
- 不捕获 OOM/StackOverflow 后继续。
长期可考虑给 child registry 增加 internal prepare/commit 两阶段 API,但 0.7.10 不应为了它重写现有稳定注册器;只有当 rollback 无法满足线性化测试时才采用。
R5. Contract 重复或错误 Cluster
风险:Get 产生歧义,调用错误服务域。
规避:
- Generator 编译期诊断。
- Build/Register 防御性 ContractType + ContractId 双重校验。
- 一个 Contract-owning assembly 一个 Cluster。
- 不提供默认 Cluster 或 fallback。
- 错误信息包含双方 cluster 和 assembly identity。
R6. 部分启动和 Host 泄漏
风险:Cluster A 已连接,Cluster B 失败,Host 启动失败但 A 仍运行。
规避:
- 所有 Slot required。
- 首次失败取消后续调度并 Stop 全部已启动 child。
- HostedService 只在全部 Ready 后发布。
- fault injection 覆盖 Connect/Handshake/Resolver/Dispose 失败。
R7. 锁顺序死锁
风险:coordinator register/stop 与 child unregister/stop 相互等待。
规避:
- 固定 coordinator -> child 的同步 writer 顺序。
- coordinator gate 内不 await。
- Stop 先 snapshot child,再释放 gate,再 await。
- child 不回调 coordinator。
- Chaos 同时执行 Stop/Register/Resolver/Streaming cancel。
R8. Cluster 数量导致连接和后台任务爆炸
风险:每个 Slot 的 MaxConnections 合法,但总和不可控。
规避:
- MaxClusters。
- MaxTotalConfiguredConnections Build gate。
- MaxConcurrentClusterConnects。
- 文档说明每个 Slot 都拥有独立 heartbeat/resolver/connection worker。
- 记录 2/8/16 Cluster 资源报告。
R9. Telemetry 标签基数
风险:cluster 名进入每调用 metric,长期内存/成本增加。
规避:
- Metrics 默认不带 cluster.name。
- Activity listener 开启时允许标签。
- 日志 scope 带 cluster。
- coordinator metric 只记录总量/状态转移。
R10. Retry 或 Breaker 穿越 Cluster
风险:业务隔离被破坏。
规避:
- 每个 Slot 使用独立现有 SharpLinkClient。
- 不把多个 Slot endpoint 压平。
- Retry exclusion mask 只存在 child 内。
- 集成测试让两个 Cluster 使用可区分响应,强制故障后验证不串流量。
R11. API 与现有 UseCluster 命名混淆
风险:用户误以为 AddCluster 和 UseCluster 是同一层级。
规避:
- 文档固定 Endpoint cluster 与 Multi-cluster client 两个术语。
- AddCluster 的 delegate 明确配置一个 child。
- XML doc 说明 child.UseCluster 只配置 Slot 内资源。
- 示例同时展示单 endpoint Slot 和 multi-endpoint Slot。
R12. NativeAOT 被动态需求拖入反射
风险:破坏现有静态 AOT 保证。
规避:
- 静态 route 使用 generated manifest + ModuleInitializer。
- 动态注册在 NativeAOT 明确 PlatformNotSupported。
- 不扫描 AppDomain assemblies。
- PackageSmoke 和独立 NativeAOT smoke 必须覆盖两个静态 Cluster。
R13. Replace 改变路由集合
风险:旧 Proxy、新 route 和 drain 状态难以线性化。
规避:
- 0.7.10 只允许相同 ContractId 集合、同 Cluster Replace。
- 改变集合必须 Unregister + Register。
- 不承诺无间隙迁移。
R14. 同一个 dependency-only Assembly 多 Cluster 所有权
风险:一个 Slot 注销导致另一个 Slot Codec 失效或 ALC 过早释放。
规避:
- ownership key 使用 (ClusterKey, Assembly object)。
- 每个 child 拥有实例级 Codec snapshot。
- coordinator 只在最后一个 owner 注销后释放自己的全局追踪。
- 专门测试先后注销顺序。
文档要求
必须新增:
- doc/architecture-0.7.10.md
- 层级图;
- route snapshot;
- child ownership;
- 注册/注销时序;
- lifecycle state;
- lock order;
- NativeAOT。
- doc/migration-0.7.10.md
- 普通 Client 无需迁移;
- 如何声明静态 route;
- 如何注册动态程序集;
- 0.7.10 限制。
- doc/performance-0.7.10.md
- baseline/candidate SHA;
- 命令、环境、五轮表格;
- P1-P4 结论。
- README
- CHANGELOG
公共 XML doc 必须明确“Cluster 选择发生在 Proxy 创建阶段,不在每次调用阶段”。
完成定义
只有同时满足以下条件,本 Issue 才能关闭:
- 所有功能和非目标边界已落实。
- 所有必须测试 checkbox 有对应自动化测试。
- 全量现有测试和跨平台门禁通过。
- NativeAOT 静态 Multi-cluster 通过。
- P1-P4 性能门禁通过并有报告。
- 无 Protocol v2 变化。
- 普通单 Client 热路径经代码审查确认无新增分支/查询/分配。
- 动态程序集注册、注销、Replace 和 ALC 释放通过竞态测试。
- 文档、示例、CHANGELOG 和 XML doc 完整。
- PR 描述提供“本 Issue 条目 -> 文件/测试/证据”的逐项映射。
背景
SharpLink dev 分支当前版本为 0.7.9。现有客户端拓扑模型支持:
当前缺口是:一个应用进程中的同一个逻辑 RPC 客户端,无法同时连接多个彼此独立的服务 Cluster,并按照 Contract 将调用固定路由到正确的 Cluster。例如:
本 Issue 定义 0.7.10 的目标、限制、建议 API、内部实现路径、并发和生命周期语义、测试矩阵、验收条件、性能门禁及风险规避策略。实现者应严格按本文逐项核对;如果确实需要偏离,必须先在 PR 描述中解释原因、兼容性和性能影响。
总体结论
新增一个独立的多 Cluster 协调层,而不是把多个 Cluster 压平到现有单个 IEndpointClusterRuntime,也不允许在每次 RPC 调用时查找 cluster。
目标结构:
每个 Cluster Slot 内部复用现有 SharpLinkClient。生成 Proxy 时,将目标 child client 或其动态模块 channel 直接传给 ProxyFactory。Proxy 创建后,RPC 热路径不得再次访问多 Cluster 路由表。
术语
为了避免与现有 UseCluster 混淆,本文使用以下术语:
必须实现的目标
G1. 多 Cluster
一个 Multi-cluster client 必须能够配置至少一个 Cluster Slot。每个 Slot 必须可以使用现有 SharpClientBuilder 支持的任一拓扑:
Slot 内部的 Retry、Load Balancing、Admission、Circuit Breaker、连接池、认证、拦截器、序列化和协议配置继续使用现有 SharpClientBuilder 语义。
G2. Contract 唯一路由
每个已暴露 Contract 必须且只能映射到一个 Cluster Slot。
client.Get() 必须:
调用 Proxy 方法之后,不允许:
G3. 编译期静态程序集路由
必须提供 Source Generator 可验证的静态程序集到 Cluster 映射。推荐 API:
语义:
必须支持 NativeAOT 的纯静态路径,不允许依赖反射扫描程序集内容。
G4. 指定 Cluster 的动态程序集注册
必须提供显式 Cluster 参数:
要求:
G5. 隔离性
Cluster 之间必须隔离:
一个 Cluster 的断连、Resolver 异常、Breaker Open、Retry 或动态拓扑替换不得将调用路由到另一个 Cluster,也不得修改其他 Cluster 的 Ready/Draining 状态。
G6. 兼容性和零禁用开销
不使用 Multi-cluster API 时:
使用 Multi-cluster API 时:
明确的非目标
以下内容不属于 0.7.10;实现者不得顺便加入:
如果未来需要上述能力,应单独设计,不得为了“预留”而给当前每调用热路径增加间接层。
公共 API 参考
名称可以在实现前进行一次统一命名调整,但语义和边界必须保持。PR 中必须列出最终 API 对照。
Cluster Key
建议新增不可变值类型,内部和公共 API 不直接散布裸 string:
验证规则:
可以提供 string 到 SharpLinkClusterKey 的便利转换,但内部快照键必须是 SharpLinkClusterKey。
Builder
参考用法:
AddCluster 的 configure 参数可以直接使用现有 SharpClientBuilder,但 Multi-cluster builder 必须通过 internal BuildCore/BuildContext 将过滤后的静态 Manifest 集合交给 child。不得先 Build 普通 child,再让每个 child 自动快照全部全局 Manifest。
Multi-cluster interface
建议:
ISharpLinkMultiClusterClient 不应继承 ISharpLinkClient,因为 ISharpLinkClient.CheckHealthAsync() 和 RegisterAssembly(Assembly) 在多 Cluster 下含义不明确。
Multi-cluster state
建议新增:
最低语义要求:
Cluster 自身的详细状态由 GetClusterState 返回。不得用协调器 State 替代 child 状态。
Options 和资源上限
建议:
要求:
静态 route manifest 设计要求
建议新增以下独立模型,不修改现有 generated assembly manifest API:
要求:
内部数据结构建议
协调器至少应有:
ClusterRouteRegistration 至少包含:
ClusterSlot 至少包含:
关键规则:
构建流程
Build 必须按以下顺序执行:
必须避免直接修改 SharpLinkClient 的每调用入口。Manifest 过滤应通过构造/BuildContext 传入,而不是在 Invoke 时检查。
Connect、运行和 Stop 语义
Connect
Get
Stop/Dispose
Lock 顺序
必须书面固定并在代码注释中记录:
禁止:
动态注册事务
RegisterAssembly(cluster, assembly) 至少按以下步骤:
为了降低复杂度,可以序列化 coordinator 动态 writer。Get 和 RPC 调用保持 lock-free。
并发竞态要求
以下竞态必须只有一个确定结果:
Unregister 和 Replace 限制
Unregister
Replace
0.7.10 限制:
Hosting 集成
必须新增与现有 Hosting 风格一致的入口,建议:
以及:
要求:
可观测性要求
必须做到:
建议文件和实现阶段
阶段 1:Abstractions 与 Generator
建议新增/修改:
先完成 Attribute、诊断、确定性生成和 catalog 测试。
阶段 2:Child Manifest 过滤
重构 SharpClientBuilder:
这一阶段不得引入 Multi-cluster per-call 分支。
阶段 3:Coordinator 与静态路由
建议新增:
完成 Build、Connect、Get、Health、Stop 和静态 route。
阶段 4:动态注册
实现 Register/Unregister/Replace、writer serialization、rollback、依赖验证和 ALC 清理。
阶段 5:Hosting、文档和性能
每个阶段都应保持可构建、测试通过;不要把全部变化堆在一个不可审查提交中。
必须完成的测试
下面是最低测试矩阵,不是可选建议。
A. Generator tests
B. Builder/unit tests
C. Contract routing tests
D. Lifecycle integration tests
至少建立两个真实 Server:
E. Dynamic assembly tests
F. Hosting tests
G. 协议和兼容测试
H. Chaos/压力测试
验收条件
功能验收必须全部满足:
性能基准门禁
性能门禁必须以实现分支从 dev 分叉时的 0.7.9 commit 作为 baseline,并在报告中记录精确 SHA、机器、OS、SDK、CPU governor/电源状态和命令。禁止只给单轮最好结果。
测量方法
Gate P1:未启用 Multi-cluster 的零回归
比较:
至少覆盖:
硬门禁:
Gate P2:启用 Multi-cluster 后的调用路径
比较:
测量阶段必须在 Proxy 已经取得后开始,排除一次性 Get 成本。
硬门禁:
Gate P3:Cluster 隔离压力
场景:
门禁:
Gate P4:启动和资源
这不是每调用热路径,但必须记录:
门禁:
性能报告必须提交到 doc/performance-0.7.10.md;大型原始 artifacts 可以继续保存在忽略目录,但 PR/CI 必须保留足够的可复现命令和摘要。
可能风险与规避路线
R1. 每个 child 自动看到全部全局 Manifest
风险:Contract 边界失效、重复字典、Codec 错误共享、动态冲突。
规避:
R2. 在每次调用时路由 Cluster
风险:吞吐下降、分配增加、动态 remap 语义复杂。
规避:
R3. 动态模块/ALC 泄漏
风险:coordinator route、错误结果、日志、Proxy 或 Codec snapshot 强引用 collectible Assembly。
规避:
R4. 注册成功但 route 发布失败
风险:child 内存在不可访问模块。
规避:
长期可考虑给 child registry 增加 internal prepare/commit 两阶段 API,但 0.7.10 不应为了它重写现有稳定注册器;只有当 rollback 无法满足线性化测试时才采用。
R5. Contract 重复或错误 Cluster
风险:Get 产生歧义,调用错误服务域。
规避:
R6. 部分启动和 Host 泄漏
风险:Cluster A 已连接,Cluster B 失败,Host 启动失败但 A 仍运行。
规避:
R7. 锁顺序死锁
风险:coordinator register/stop 与 child unregister/stop 相互等待。
规避:
R8. Cluster 数量导致连接和后台任务爆炸
风险:每个 Slot 的 MaxConnections 合法,但总和不可控。
规避:
R9. Telemetry 标签基数
风险:cluster 名进入每调用 metric,长期内存/成本增加。
规避:
R10. Retry 或 Breaker 穿越 Cluster
风险:业务隔离被破坏。
规避:
R11. API 与现有 UseCluster 命名混淆
风险:用户误以为 AddCluster 和 UseCluster 是同一层级。
规避:
R12. NativeAOT 被动态需求拖入反射
风险:破坏现有静态 AOT 保证。
规避:
R13. Replace 改变路由集合
风险:旧 Proxy、新 route 和 drain 状态难以线性化。
规避:
R14. 同一个 dependency-only Assembly 多 Cluster 所有权
风险:一个 Slot 注销导致另一个 Slot Codec 失效或 ALC 过早释放。
规避:
文档要求
必须新增:
公共 XML doc 必须明确“Cluster 选择发生在 Proxy 创建阶段,不在每次调用阶段”。
完成定义
只有同时满足以下条件,本 Issue 才能关闭: