Skip to content

[api][multi-cluster] 为 Add/Replace/Remove 提供各自的 structured result contract #656

Closed

Description

@SunSi12138

背景

Related: #37, #86, #652, #653

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

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。

非目标


完成条件

Activity

  1. SunSi12138 commented on Sep 12, 2026

    @SunSi12138
    OwnerAuthor

    #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/unavailable SharpLinkErrorCode 分类,本地 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 收口。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions