Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4509f59
docs: design 0.7.5 static endpoint cluster
Jul 21, 2026
76c4ba3
feat: add static multi-endpoint client clusters
Jul 21, 2026
aa25533
test: cover static endpoint transport clusters
Jul 21, 2026
51303ac
fix: enforce static endpoint factory ownership
Jul 21, 2026
7f12700
fix: retain folded endpoint identity
Jul 21, 2026
dae375b
test: cover static TLS endpoint authority
Jul 21, 2026
c9393d1
test: verify static cluster stop convergence
Jul 21, 2026
75f39b7
test: verify static endpoint selector routing
Jul 21, 2026
3b60663
test: validate least-pending static selection
Jul 21, 2026
b6be04c
fix: harden static cluster draining and selection
Jul 21, 2026
8ea3072
test: add static endpoint performance matrix
Jul 21, 2026
e1ac260
docs: record 0.7.5 performance evidence
Jul 21, 2026
8b1a2da
chore: prepare local 0.7.5 development version
Jul 21, 2026
105c8f8
feat: add dynamic endpoint resolver discovery
Jul 21, 2026
66427f5
feat: complete SharpLink 0.7 resiliency stack
Jul 21, 2026
3fd51bd
fix: address topology and admission review findings
Jul 21, 2026
f87a39f
fix: complete cluster lifecycle cleanup
Jul 21, 2026
fee964f
fix: complete endpoint admission lifecycle
Jul 21, 2026
a207d46
fix: converge dynamic topology lifecycle
Jul 21, 2026
b65fa0d
fix: coordinate cluster recovery budgets
Jul 21, 2026
e15099e
fix: coordinate endpoint recovery fairness
Jul 21, 2026
ae9c678
fix: clear stale admission delays
Jul 21, 2026
fb60f6c
fix: bound endpoint recovery coordination
Jul 21, 2026
e1667d8
fix: preserve endpoint recovery and outcomes
Jul 21, 2026
97ded1b
fix: harden admission retry recovery
Jul 21, 2026
e625d6b
fix: report goaway to circuit breaker
Jul 21, 2026
1b51349
fix: reserve initial dial capacity
Jul 21, 2026
87ab5c1
fix: await static cluster recovery
Jul 22, 2026
cf0c41b
fix: preserve cluster recovery contracts
Jul 22, 2026
0f9dffa
fix: preserve endpoint failure outcomes
Jul 22, 2026
559a320
feat: complete endpoint telemetry coverage
Jul 22, 2026
053b5fe
fix: preserve client topology telemetry semantics
Jul 22, 2026
71f8850
fix: cancel retry waits during client stop
Jul 22, 2026
5f20e98
fix: preserve cluster recovery invariants
Jul 22, 2026
1e67a08
fix: preserve dynamic recovery edge cases
Jul 22, 2026
bcccdc1
fix: isolate cluster endpoint failures
Jul 22, 2026
8bee284
fix: preserve dynamic topology capacity
Jul 22, 2026
139759b
fix: continue probing initial endpoint dials
Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,83 @@

## [Unreleased]

## [0.7.9] - 2026-07-21

### 收敛

- endpoint 路径补充低基数 `sharplink.client.attempts`、`retries`、`endpoint_admission.rejected`、`breaker.open` 与 `selection.failures` metrics;默认标签不含 endpoint ID、地址、authority 或 transport 名称。
- 完成 static/dynamic topology、selector、Retry、custom admission 与 generation-scoped circuit breaker 的本地组合验证;物理 Ready/Draining 状态不因 admission 拒绝或 breaker Open 被伪装成断线。
- 文档补全迁移路径、传输限制、Retry/Breaker 语义与 0.7.x API freeze 审核说明。

## [0.7.8] - 2026-07-21

### 新增

- `ISharpLinkEndpointAdmissionPolicy`、`SharpLinkEndpointAdmissionDecision` 和 `SharpLinkEndpointOutcome` 提供 endpoint 级 TryAcquire/Report SPI;`UseEndpointAdmission` 显式启用自定义策略。
- `UseCircuitBreaker` 和 `SharpLinkCircuitBreakerOptions` 提供按 endpoint generation 隔离的 Closed/Open/HalfOpen breaker,默认关闭。

### 变更

- endpoint selection 在连接选择之前执行 admission;拒绝候选会继续选择其他 Ready endpoint,实际获得许可的 attempt 沿用 PendingCall 单一终结路径恰好 Report 一次。
- breaker 使用 monotonic time、惰性状态推进、有限采样 ring 和 HalfOpen 原子 permit,不创建每 endpoint timer;连接、拓扑与 `CheckHealthAsync` 的物理语义保持不变。

## [0.7.7] - 2026-07-21

### 新增

- `UseRetry()`、`UseRetry(Action<SharpLinkRetryOptions>)` 和 `UseRetry(ISharpLinkRetryPolicy)` 为显式标记 `[Idempotent]` 的 Unary 提供可选重试;默认最多三次、50/100/200 ms 指数退避和 ±20% jitter。
- `ISharpLinkRetryPolicy`、`SharpLinkRetryContext` 与 `SharpLinkRetryDecision` 提供同步、无 I/O 的自定义决策 SPI;非法 delay 或 policy 异常只失败当前 logical call,不影响 Client 健康。
- Retry attempt 复用既有 PendingCall 的单一完成仲裁,记录 endpoint/generation、connection、完成原因、响应是否已观测和耗时;无第二套 pending-request 表。

### 变更

- Client interceptor 仍只对一次 logical call 执行;Retry 位于其 terminal 内,每次 attempt 重新选择 endpoint,并共享入口冻结的绝对 deadline。
- 默认策略仅对 `[Idempotent]` Unary 的 endpoint 不可用、连接关闭/切换、发送失败与远端 `Unavailable` 重试;业务错误和 `ResourceExhausted`、OneWay 及所有 Streaming 不自动重试。
- 多 endpoint retry 使用调用内 `ulong` exclusion mask,优先尝试不同 Ready endpoint;尝试完当前 snapshot 后才复用候选,动态 generation 更新自动形成新候选集。
- telemetry 保持 logical call 指标一调用一次,并在 listener 存在时额外产生 `sharplink.rpc.attempt` Activity。

### 兼容性与验证

- Retry 默认关闭,因此固定单 endpoint 的既有 Unary 继续直接走原有路径;Protocol v2 wire format 和握手 capability 未改变。
- 覆盖远端 `Unavailable`、`ResponseObserved`、非幂等与 `ResourceExhausted` 拒绝重试、绝对 deadline、delay 中取消、custom policy、interceptor 一次性和 endpoint exclusion/reset。

## [0.7.6] - 2026-07-21

### 新增

- `SharpLinkEndpointSnapshot`、`ISharpLinkEndpointResolver` 与 `UseEndpointResolver` 提供版本化的动态 endpoint 拓扑;Client 对 Resolver 拥有明确的 Stop/Dispose 生命周期。
- `DelegateSharpLinkEndpointResolver` 支持连续 Watch 或单 worker 轮询,可适配 Consul、Nacos、Etcd 等应用已有 SDK,而 SharpLink 核心不引入其依赖。
- `UseDnsEndpoints` 与 `SharpLinkDnsEndpointResolver` 提供 A/AAAA Discovery、地址族筛选、规范化稳定 ID、hostname Authority、last-good 保留和可配置 refresh/jitter。

### 变更

- 动态快照以单 writer 原子协调:新增 ID 建立 generation;同 ID 的 Address/Authority 变化替换 generation;Attributes-only 更新保留连接;删除 endpoint 立即停止新调用并排空已有 Unary/Streaming。
- Resolver Watch 结束或异常后以 100 ms–30 s 的指数退避和 ±20% jitter 重启;空拓扑可恢复且继续遵守 WaitForReady、deadline、cancel 与 Stop。
- retired connection 使用独立预算。预算超出时抑制 replacement 而不强杀用户 stream,归零后 factory 恰好释放一次。

### 兼容性与验证

- Protocol v2 wire format、握手 capability、固定单 endpoint 与静态 cluster 的调用路径未改变;无新 NuGet 或第三方服务发现 SDK。
- 覆盖 add/remove/replace、属性更新、DNS、watch/retry、流排空、PackageSmoke、NativeAOT 及动态稳态矩阵;固定 TCP 五轮 A/B 的 QPS 中位数为 0.7.5 的 100.44%,P99 中位数保持 72 µs。

## [0.7.5] - 2026-07-21

### 新增

- Client 新增不可变的 `SharpLinkEndpoint`、显式传输地址和 transport factory 注册模型;`UseEndpoint`、`UseEndpoints`、`UseCluster` 及四种内置负载均衡策略可在不影响旧单端点用法的前提下构建静态端点集群。
- 静态集群支持 TCP(hostname/IPv4/IPv6)、Unix Domain Socket、Named Pipe、Shared Memory 和既有 Anonymous Pipe;端点属性可传给自定义选择器。
- 集群按端点独立维护连接、重连和健康状态,初始连接受 `MaxConnections` 与并行度上限约束;支持最少就绪端点、LeastPending、P2C、Random、RoundRobin 与自定义选择器。

### 变更

- `GoAway`、Stop 和 Dispose 进入排空流程后,新调用立即选择仍健康的端点;长 Unary 和流式调用在预算内继续完成,超出独立 retiring budget 时才被定点终止。
- 单个静态端点继续折叠为既有固定连接快速路径;多端点的成员快照仅在就绪成员增减时重建,调用路径仅读取原子快照和实时计数。
- Protocol v2 wire format、默认传输语义和现有公共配置保持兼容,未引入新的 NuGet 依赖。

### 测试与性能

- 覆盖多传输、地址族、故障/重连、GoAway 排空、所有选择策略、TLS、包使用与 NativeAOT;固定 TCP Unary 的五轮本地 A/B 中,吞吐中位数为基线的 100.27%,P99 为 104.48%,BenchmarkDotNet 分配保持 352 B/op。

## [0.7.4] - 2026-07-20

### 新增
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>
<!-- Nuget包信息 -->
<PropertyGroup>
<Version>0.7.4</Version>
<Version>0.7.9</Version>
<Authors>sunsi</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,13 @@ if (health.Status != SharpLinkHealthStatus.Ready)
- 0.7.1 迁移:`doc/migration-0.7.1.md`
- 0.7.2 性能与迁移:`doc/performance-0.7.2.md`、`doc/migration-0.7.2.md`
- 0.7.4 压缩、接入控制与性能:`doc/migration-0.7.4.md`、`doc/performance-0.7.4.md`
- 0.7.5 静态多 endpoint:`doc/architecture-0.7.5.md`、`doc/performance-0.7.5.md`
- 0.7.6 动态 endpoint、Resolver 与 DNS Discovery:`doc/architecture-0.7.6.md`
- 0.7.6 本地性能证据:`doc/performance-0.7.6.md`
- 0.7.7 logical call、attempt 与 retry:`doc/architecture-0.7.7.md`
- 0.7.8 endpoint admission 与 circuit breaker:`doc/architecture-0.7.8.md`
- 0.7.9 迁移、组合验证与 API freeze:`doc/migration-0.7.9.md`
- 0.7.9 本地性能与组合 smoke:`doc/performance-0.7.9.md`
- 0.6.10 性能与 Chaos:`doc/performance-0.6.10.md`、`doc/chaos-0.6.10.md`
- 贡献指南:`CONTRIBUTING.md`
- 更新日志:`CHANGELOG.md`
57 changes: 57 additions & 0 deletions doc/architecture-0.7.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SharpLink 0.7.5 静态多端点设计

本文档是 0.7.5 的实现设计。它补充 `architecture.md`,在 0.7.5 全部验收前不描述 0.7.6 的动态 Resolver、0.7.7 的 Retry 或后续韧性策略。

## 目标与非目标

- 保留既有 `UseTransport` 和所有 `UseTcp`、`UseUds`、`UseNamedPipe`、`UseAnonymousPipe`、`UseSharedMemory` 路径的固定单端点快路径。
- 通过 `UseEndpoint` 或 `UseEndpoints` 显式启用静态 endpoint 配置;单个 endpoint 在 `Build()` 时折叠为固定快路径,两个或更多 endpoint 才构造 Cluster 状态。
- 一个 Cluster Client 继续只拥有一套 proxy、manifest、interceptor、runtime context 和调用管线。endpoint 不是子 Client。
- 动态成员变更、Resolver、Retry、Admission、Circuit Breaker 和新增 wire capability 均不在 0.7.5 范围内。

## 公共模型

`SharpLink.Abstractions` 定义不可变的 `SharpLinkTransportAddress` 记录层次、`SharpLinkEndpoint`、endpoint transport factory 委托和 selector SPI。地址和值对象在构造时验证自身不变量;endpoint 的 ID、属性个数和属性键值长度在 Builder/Cluster snapshot 边界验证。

endpoint 进入 Client 时将被深复制:`Id`、`Address` 和 `Authority` 是不可变值,Attributes 复制为只读字典。Client 不保留调用方提供的 collection 或 dictionary 引用。`Address + Authority` 是连接 generation 的身份;0.7.5 的静态拓扑只有 generation `1`,但候选和诊断保留 generation 字段,使该事实不泄漏到调用路径。

`SharpLinkEndpointSelectionContext` 为只读 `ref struct`,只携带当前不可变 Ready candidate snapshot、数量和 `ulong` exclusion mask。selector 返回该 snapshot 的索引;越界、被排除或失去 Ready 状态的索引使当前调用以 `FailedPrecondition` 失败,selector 异常只失败当前调用。一个候选时不调用随机数或用户 selector。

## Builder 运行模式

Builder 在 `Build()` 时一次性冻结为下列模式之一:

```text
FixedTransport
UseTransport / legacy transport helpers
UseEndpoint(s) with exactly one endpoint

StaticEndpoints
UseEndpoint(s) with two to 64 endpoints
```

`UseTransport` 与 `UseEndpoint(s)` 互斥。固定模式允许 `UseConnectionPool`,不允许 `UseCluster`;集群模式要求 `UseCluster` 或使用其默认快照,不允许 `UseConnectionPool`。Cluster 的连接总量和 endpoint 内连接数分别由 `MaxConnections`、`MaxConnectionsPerEndpoint` 约束;Connecting 和 Ready 都计入总预算,retiring 连接使用独立预算。

每次调用 endpoint transport factory 仅由 Client 所有。每个 factory 只会在该 endpoint 的停止/清理路径调用一次 `DisposeAsync`。内置 factory helper 位于 `SharpLink.Client`,将 TCP、UDS、NamedPipe 和 SharedMemory 地址映射到现有 transport;AnonymousPipe 的一次性 handle offer 不支持内置多 endpoint 配置。

## Cluster 状态机与连接所有权

Static Cluster 的 Client 拥有一个 `StaticClusterRuntime`:冻结的 `EndpointState[]`、不可变 Ready candidate 数组、全局连接 reservation、retiring connection 预算、一次性 topology signal 和已跟踪 worker 集合。每个 `EndpointState` 拥有一个 endpoint generation 的 factory、Ready connection snapshot、Connecting/Ready/Draining 计数、active-call 计数、reconnect 信号和该 endpoint 的 worker。

连接建立采用 endpoint-first:在全局预算内先尝试让各 endpoint 各有一条连接,之后才扩展同一 endpoint。最多四个内部连接操作并发。`ConnectAsync` 在任意 endpoint 完成 RPC handshake 后成功;后台继续填充实际 `min(MinReadyEndpoints, endpointCount)`。任一 endpoint 的失败只启动自身退避重连,不阻塞其余 Ready endpoint。Stop 获胜后取消所有 endpoint worker,等待其退出,释放 connection 与 factory,且不会再建立连接。

Ready candidate snapshot 只包含至少一条 Ready connection 的 endpoint。它仅在启动完成、连接数在零与非零间转换、或 endpoint 固定成员初始化时重建,并以 `Volatile.Write` 作为同一 endpoint/candidate 对发布。调用路径只读取这一快照与原子计数,不取得 topology writer lock,也不重建数组。Ready/active 计数通过 endpoint-owned provider 实时读取,因此连接扩缩容或 in-flight call 变化不会迫使快照重建。

## 选择与调用

调用先从 Cluster Ready candidate snapshot 选择 endpoint,再用该 endpoint 内已有的连接 P2C 选择 connection。P2C 用两个不同候选的 `ActiveCallCount / ReadyConnectionCount` 作 64 位交叉相乘比较;Random、RoundRobin 和 LeastPending 也只在静态 snapshot 上运行。RoundRobin cursor 是 Client 实例字段,LeastPending 使用旋转起点消除固定并列偏差。

endpoint 在选择后失去最后一个 Ready connection 时,该调用在同一 snapshot 上设置 exclusion bit 并重新选择,最多候选数次;无需 `HashSet`。所有 endpoint 不可用时保留既有 `WaitForReady`、deadline、cancellation 与 Stop 语义。Streaming/OneWay 在开始时选择一个 connection 后一直绑定它;GoAway 仅排空所属 endpoint 中的对应 connection。Retiring connection 不消耗 Ready/Connecting budget,最多保留 `MaxRetiringConnections` 条;超出该独立预算的连接会被关闭并让当前调用得到连接关闭结果。

## 验收映射

- 公共 address/endpoint/selector/options API 由 Unit 与 PackageSmoke 覆盖。
- Builder 模式、冻结和 factory ownership 由 Unit 覆盖。
- P2C、Random、RoundRobin、LeastPending 与 selector failure 由无网络 Unit 覆盖。
- TCP、UDS、NamedPipe、SharedMemory 多 endpoint 分布、局部故障、独立重连、GoAway、Stop/Dispose 并发由 Integration 覆盖。
- fixed single 与 one-static-endpoint 继续使用原路径;性能结论只在完整 0.7.5 矩阵完成后写入性能报告。
47 changes: 47 additions & 0 deletions doc/architecture-0.7.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SharpLink 0.7.6 动态端点与 Resolver 设计

本文档描述 0.7.6 在 0.7.5 静态 endpoint cluster 之上增加的动态拓扑能力。它不改变 Protocol v2,不新增握手 capability,也不改变固定单端点或静态 cluster 的调用路径。

## 启用方式与边界

动态模式必须显式配置,且与 `UseTransport`、`UseEndpoint`、`UseEndpoints` 互斥:

```csharp
var client = SharpClientBuilder.Create()
.UseEndpointResolver(resolver, SharpLinkTransportFactories.Sockets())
.UseCluster(options => options.MaxConnections = 4)
.Build();
```

`ISharpLinkEndpointResolver` 位于 `SharpLink.Abstractions`;它只返回完整的 `SharpLinkEndpointSnapshot`,不创建或持有 transport factory。Client 拥有 resolver,并在 Stop/Dispose 中恰好调用一次 `DisposeAsync`。静态与固定模式不会创建 resolver worker、候选数组或额外调用路径。

`SharpLinkEndpointSnapshot.Version` 必须严格递增。Client 对 `version <= lastAcceptedVersion` 的快照直接忽略;新快照先完整复制、冻结并校验,再做 reconciliation。重复 ID、非法地址、超出 `MaxEndpoints` 或属性限制会使整份快照被拒绝,最后一个成功拓扑继续服务。空快照合法,它会移除全部新调用候选,但仍允许 `WaitForReady` 等待后续恢复。

## Resolver 生命周期

`ConnectAsync` 先调用一次 `ResolveAsync`,接受初始拓扑后启动一个有界 watch worker。Worker 正常结束、抛出异常或 Resolve 失败时保留 last-good topology,并按 100 ms 起步、最大 30 s、±20% jitter 的退避重试。任一次成功 Resolve 或 Watch 更新都会复位退避。

Stop 先取消 Client 生命周期 token;watch、resolver retry 和 endpoint reconnect 都以该 token 为退出条件。Stop 获胜后不再接受快照或创建连接,随后等待 worker、释放 connection/factory 并 dispose resolver。`DelegateSharpLinkEndpointResolver` 可桥接任意 Consul、Nacos 或 Etcd SDK:提供 watch delegate 时直接转发;没有 watch 时只以一个有界 polling delay 调用 resolve delegate。

## 拓扑协调与 generation

动态 runtime 维护一个单 writer 的 current topology 和独立的 retired generation 集合:

- 新 ID:创建新的、Client 所有的 factory 与单调递增 generation。
- 相同 ID 且 `Address + Authority` 相同:复用连接和 factory,仅替换冻结后的 Attributes。
- 相同 ID 但地址或 Authority 改变:先发布新 generation,再使旧 generation 退出候选并进入 draining。
- 删除 ID:在同一次原子发布中从候选移除,已有调用与 stream 继续绑定旧 connection 至完成。

Ready endpoint/candidate 对以一次 `Volatile.Write` 发布。属性更新也强制重建控制面 candidate snapshot,使自定义 selector 立刻读取新的属性;连接数、active calls 和选择过程仍不获取 topology writer lock。稳定调用不因 resolver 无更新而分配 topology collection。

Retiring connection 不计入 active Ready/Connecting budget。超过 `MaxRetiringConnections` 时,拓扑仍然接受更新,但抑制新的 replacement connection,而不会强杀用户 stream;当 draining connection 和 active call 归零后,旧 factory 恰好释放一次。

## 内置 DNS Discovery

`UseDnsEndpoints(host, port, factory, configure)` 使用 `SharpLinkDnsEndpointResolver`。它查询 A/AAAA(可按 `AddressFamily` 过滤),对规范化 IP 去重排序,并由 host、port、address family 和 IP 构造稳定 endpoint ID;原始 host 保留为 Authority,因此 TLS 默认 SNI/证书主机名不随 IP 变化。地址排列变化不会发布 snapshot;地址增加/消失分别表现为 add/remove。

BCL 无法提供可移植的 DNS TTL,因此 resolver 使用 `RefreshInterval`(由最小/最大 interval 约束)与可选 jitter,不伪造 TTL。查询失败保留 last-good 结果。DNS 查询器在内部可替换,测试不依赖公网 DNS;一个 resolver 只运行一个 refresh loop,不创建每 endpoint timer。

## 验证范围

0.7.6 的 Unit/Integration/PackageSmoke 覆盖 DNS 规范化和 last-good、resolver 所有权、初始空拓扑恢复、add/remove、同 ID 地址 generation 替换、Attributes-only 更新、正常 Watch 结束、resolver failure/retry、流排空和新调用迁移。所有行为都是客户端本地行为,继续与 0.7.4 Server 互操作,且保持 NativeAOT 可用。
Loading
Loading