Repository navigation
[api][multi-cluster] 为 Add/Replace/Remove 提供各自的 structured result contract #656
Description
背景
SharpLink 的 multi-cluster runtime mutation 当前 public surface 不一致:
ValueTask AddClusterAsync(...);
ValueTask ReplaceClusterAsync(...);
ValueTask<SharpLinkClusterRemovalResult> RemoveClusterAsync(...);RemoveClusterAsync 已经有 operation-specific result;AddClusterAsync / ReplaceClusterAsync 的预期失败主要仍通过 exception 表达。
#652 单独处理 Add 的 lifecycle 语义:Running Client 新增 cluster 时,不要求 remote 立即 Ready。这个 issue 不重复该行为设计,而是聚焦 mutation public contract。
目标不是为了 API 对称性造一个通用大 ClusterMutationResult,而是让每个操作拥有符合自身语义的轻量 structured result。
目标
建议 public 方向:
ValueTask<SharpLinkClusterAddResult> AddClusterAsync(...);
ValueTask<SharpLinkClusterReplacementResult> ReplaceClusterAsync(...);
ValueTask<SharpLinkClusterRemovalResult> RemoveClusterAsync(...);三者可以复用少量稳定 failure code,但不要强制共享一个包含大量 nullable 字段的 giant result type。
统一的是:
- 调用方可以机器可读地分支 expected rejection;
- 不依赖解析 exception message;
- publication outcome 与 cleanup/retirement outcome 明确区分;
- programmer error / cancellation / invariant failure 仍保留 exception。
1. SharpLinkClusterAddResult
Add 的核心结果是 publication 是否提交。
#652 之后,remote 暂时不可用不应是 Add failure;cluster 可以成功发布并处于 Connecting/Reconnecting/NotReady。
结果至少应能够表达类似:
Succeeded
AlreadyExists
Busy
LifecycleClosed
RouteConflict
CapacityExceeded
CandidateRejected / LocalPreparationFailed(仅在确有稳定分类价值时)
不应把这些预期 rejection 继续要求 caller 用 InvalidOperationException / ArgumentException message 分支。
建议结果保持简单:
Succeeded
FailureCode
optional diagnostic message
不要把 readiness 塞进 AddResult;发布后的 connectivity/readiness 已有独立状态 API。
2. SharpLinkClusterReplacementResult
Replace 与 Add 不同:它合理保持 availability-first / ready-before-swap。
典型流程:
old cluster remains published
-> prepare replacement
-> start/connect replacement
-> replacement Ready
-> atomic swap
-> retire old cluster
因此 result 至少要区分两个阶段:
A. replacement 未 publish
candidate connect/handshake failed
-> old cluster remains authoritative
B. replacement 已 publish
old cleanup/drain timed out or forced stop continued
-> replacement itself still succeeded
建议能够表达:
Succeeded
Published
FailureCode
ReferencesReleased
ForcedStop
具体字段可以更精简,但必须避免用一个 Succeeded=false 同时混淆“没换成功”和“已经换成功,只是旧资源清理未完成”。
预期 failure classification 可包括:
NotFound
Busy
LifecycleClosed
RouteConflict
CapacityExceeded
CandidateUnavailable
CandidateRejected
Candidate transport/TLS/handshake unavailable 属于一次正常可处理的 replacement attempt failure;旧 cluster 没被替换时,caller 通常会记录、重试或放弃,不应强制 exception control flow。
3. SharpLinkClusterRemovalResult
现有类型已经有正确方向:
Succeeded
ReferencesReleased
ForcedStop
本 issue 不要求推翻它。
只需要审计 expected rejection 是否仍从内部直接抛出,例如:
valid key but cluster not found
mutation busy
lifecycle closed
这些如果属于正常 control-plane 分支,应补进 removal result contract。
明显非法 cluster key、负 graceful timeout 等 programmer error 继续用 argument exception。
Failure code 复用
可以考虑一个很小的共享 enum,例如概念上:
enum SharpLinkClusterMutationErrorCode
{
None,
AlreadyExists,
NotFound,
Busy,
LifecycleClosed,
RouteConflict,
CapacityExceeded,
CandidateUnavailable,
CandidateRejected
}但最终只保留实际有稳定调用价值的 code;不要为了完整枚举制造没有现实来源的状态。
也允许 Add/Replace/Remove 各自拥有更窄的 error enum,如果这样能让 API 更清楚且不会产生大量重复类型。
原则:
operation-specific result,shared concepts only where they are genuinely shared。
与 #652 的关系
#652 负责:
Running AddCluster
-> Start candidate runtime
-> publish
-> background connectivity converges
本 issue 负责:
AddCluster 的 expected transaction rejection
-> structured AddResult
不要把 remote NotReady 重新定义成 AddResult failure。
#652 中已经明确的 Add/Replace activation policy、Running + aggregate Connecting、budget preflight/revalidation 等 correctness contract 保持不变。
Exception 边界
继续允许 exception:
ArgumentNullException
ArgumentOutOfRangeException
明显非法 cluster key / builder 参数
OperationCanceledException
internal invariant violation
unexpected cleanup/runtime fatal failure
应用 configure callback 自己抛出的异常
优先 structured result:
AlreadyExists
NotFound
Busy
LifecycleClosed
RouteConflict
CapacityExceeded
CandidateUnavailable
正常 bounded cleanup outcome
不要为了“无异常”吞掉真正 unexpected failure。
Cancellation / commit point
result model 不能模糊现有事务边界。
Add:
before publish cancellation
-> operation canceled / rollback
after publish
-> publication 已提交,caller cancellation 不撤销 cluster ownership
Replace:
before swap cancellation
-> old remains authoritative
after swap
-> replacement 已发布;old retirement 继续由 coordinator owner 完成
Remove:
unpublish 后
-> cleanup outcome 可单独表示 ReferencesReleased / ForcedStop
Caller cancellation 仍然使用 OperationCanceledException,不包装成普通 failure code。
API / performance 约束
- result 优先使用小型 immutable value type;
- caller 不依赖 diagnostic message 做机器分支;
- 不创建 generic
Result<T>framework; - 不把 Add/Replace/Remove 强塞成一个 giant union;
- 不因为 result model 修改 RPC routing hot path;
- 不改变 snapshot publication / transition budget / retirement ownership;
- docs/XML 清楚标注 expected result 与 exceptional failure 边界。
Validation
至少覆盖:
Add
- AlreadyExists / Busy / LifecycleClosed / RouteConflict / CapacityExceeded 有稳定 structured outcome;
- [client][multi-cluster][lifecycle] Running 时 AddCluster 不应要求远端立即 Ready #652 的 remote-not-ready Add 仍可
Succeeded=true; - failed Add 不污染 snapshot / route / budget;
Replace
- valid target + candidate unavailable 返回 structured replacement failure,old cluster 保持可用;
- successful swap 与 old cleanup timeout 可以同时表达“replacement succeeded + forced/pending cleanup”;
- NotFound / Busy / LifecycleClosed / budget conflict 可机器分支;
- publication 前失败不改变 old route;
Remove
- valid-but-not-found / Busy / LifecycleClosed 不需要 exception 做正常分支;
- existing
ReferencesReleased/ForcedStop语义保持;
Common
- invalid arguments 仍抛标准 argument exception;
- caller cancellation 仍为
OperationCanceledException; - mutation commit point / rollback / background cleanup ownership 不变;
- no RPC hot-path regression。
非目标
- 不重做 [client][multi-cluster][lifecycle] Running 时 AddCluster 不应要求远端立即 Ready #652 的 Add lifecycle 行为;
- 不把 Replace 改成 publish-before-ready;
- 不改变 reconnect/readiness 状态机;
- 不统一成单个 giant
SharpLinkClusterMutationResult; - 不重做 runtime configuration result([api][runtime-config] 运行时配置的预期拒绝改为 structured result #654);
- 不重做 health result([api][health] CheckHealthAsync 对 NotReady/Unavailable/Unsupported 返回结构化结果而非异常 #655);
- 不重做 RPC
SharpLinkExceptionerror model。
完成条件
- Add / Replace / Remove 各自拥有清晰、operation-specific structured result contract;
- expected mutation rejection 不再依赖 exception 类型/message 作为正常控制流;
- Replace 明确区分 pre-publication failure 与 post-publication retirement outcome;
- Remove 现有 cleanup result 语义得到保留/完善;
- programmer error、cancellation、invariant/fatal failure 保持异常语义;
- 与 [client][multi-cluster][lifecycle] Running 时 AddCluster 不应要求远端立即 Ready #652 / [api][control-plane] 统一预期运行失败为 structured result,避免异常作为正常控制流 #653 / [Runtime Architecture][Phase 18] 完成 public API/package 审计、发布门禁与迁移文档 #86 的 lifecycle、expected-failure、public API 方向一致。
Activity
SunSi12138 commentedon Sep 12, 2026
#662 已在
dev完成并 squash merge:1e7a10c27d3b76c505a9db784ed6c41b3334035e。完成内容:Add / Replace / Remove 现在分别返回 operation-specific structured result,并共享小型稳定
SharpLinkClusterMutationFailureCode;expected control-plane rejection 不再要求解析 exception message。Add 成功只表示 publication committed;Replace 明确区分 pre-publication rejection 与 post-publication old-generation retirement outcome;Remove 保留并扩展既有 cleanup result。Review 中发现的 replacement availability 分类过宽也已修复:
CandidateUnavailable只对白名单 auth/connect/unavailableSharpLinkErrorCode分类,本地ResourceExhausted、Internal、DataLoss、ProtocolViolation等继续保持 exceptional;已有真实 oversized-auth-payload regression 锁定该边界。Caller cancellation、invalid arguments、configuration/invariant failure、unexpected cleanup/runtime failure 均继续使用异常语义。无 development version/schema bump。#656 可以按 completed 收口。