目标
验证 Client 在启用 Retry、Endpoint Admission、Circuit Breaker 后产生的每逻辑调用 / 每 attempt 固定成本 ,并只对有证据的 state/ownership 做最小优化。
历史正式 evidence:
Fixed default ~952 B/op
Retry first success ~1512 B/op (+560)
AlwaysAcceptAdmission ~1176 B/op (+224)
ClosedCircuitBreaker ~1176 B/op (+224)
这些数字和当前对象模型高度相关,但不能直接把差值等同于某个 class。执行时必须重新读取当前 dev HEAD、重新做 allocation stack,最终 macro 使用 #122 。
本 issue 要回答:
为了表达 retry selection、attempt outcome、admission lease/report、response-observed 与 elapsed,当前是否让“第一 attempt 就成功”的正常调用承担了过重对象状态?
依赖
#81 可能删除/重命名重复 active counter;本 issue 不得提前把 counter 语义写死。可以先测 object/state,production merge 前必须 rebase/re-audit #81 结论。
当前 dev 事实
Retry path
InvokeUnaryWithRetryAsync 当前会为一个 logical retry call 创建:
var selection = new EndpointRetrySelectionState ( ) ;
然后每次 attempt:
var outcome = new AttemptOutcomeState ( this , method ) ;
即使第一 attempt 成功 ,这些状态也已经创建。
Endpoint admission / breaker without retry
默认 Unary core 在 _endpointAdmissionPolicy != null 时也会创建:
new AttemptOutcomeState ( this , method )
因此 AlwaysAcceptAdmission / ClosedCircuitBreaker 都会付 attempt-state 成本。
AttemptOutcomeState 当前职责很多
它同时保存:
client/method;
attempt start timestamp;
endpoint start timestamp;
completion reason;
response observed;
local error code;
admitted endpoint candidate;
admission token;
admission lease/report flags;
grant/reject sequence;
retry-after;
endpoint/connection diagnostic identity;
PendingCall completion observer role。
这说明它不是可以直接删除的“DTO”。真正问题是:
所有 feature combination 是否都需要同一套最重状态?
第一性原理
1. 只为启用能力付成本
理想模型:
retry only
-> 只付 retry真正需要的状态
admission only
-> 只付 admission/report需要的状态
retry + admission
-> 付组合状态
不应该因为一个大 class方便实现,让 retry-only 为 admission token/sequence/report 字段付完整对象成本。
2. Logical retry state 与 endpoint admission lease 是不同生命周期
Retry selection mask跨 attempt;Attempt outcome通常只对应一个 physical attempt;PendingCall completion observer可能比调用栈活得更久。
不要把它们为了“少对象”强行合成一个更难证明的大状态机。
3. Stale callback 是最重要风险
如果 attempt state进入 pool:
旧 PendingCall callback
-> state 已归还并被新 attempt Rent
-> callback污染新 attempt
这是不可接受的 ABA/lifecycle bug。
因此 pooling 只有在 terminal ownership和 generation明确后才允许。
4. Retry first success 是最重要 control
正常健康系统最常见:
Retry configured
Attempt 1 succeeds
如果启用 retry 就固定 +500B,即使永远不真正 retry,也值得优化。
5. Policy semantics 高于 allocation
不能为了少对象改变:
endpoint exclusion;
admission token/report exactly once;
RetryAfter;
response-observed;
circuit breaker success/failure统计;
same attempt endpoint identity;
retry decision error code。
Step 0:baseline
git checkout dev
git pull --ff-only
git rev-parse HEAD
dotnet --info
运行 Release build/unit/integration,特别覆盖 retry/circuit breaker/endpoint admission tests。
Step 1:建立 feature matrix microbenchmark
必须把能力拆开:
Default
RetryConfigured_FirstSuccess
RetryConfigured_FirstFailsSecondSucceeds
AdmissionAlwaysAccept
AdmissionAlwaysReject
CircuitBreakerClosed
CircuitBreakerOpen
Retry+Admission_FirstSuccess
Retry+Admission_RetryOnce
WaitForReady_NoWait
同一个 tiny Unary codec/transport fixture,避免 payload差异。
记录:
B/logical call;
B/attempt;
objects/call;
ns/call;
CPU;
contention。
Step 2:allocation stack 必须拆出具体 owner
至少确认:
EndpointRetrySelectionState
AttemptOutcomeState
SharpLinkEndpointOutcome
SharpLinkRetryContext / decision(若有 allocation)
Activity/telemetry attempt scope
PendingCall observer相关 state
async retry state machine
Task.Delay/retry timer only when actual retry
尤其要分清:
first-success固定成本;
actual second-attempt才出现的成本;
admission policy内部成本;
SharpLink wrapper成本。
不能把用户 policy 自己分配的对象归到 Runtime。
Step 3:Candidate A — EndpointRetrySelectionState 改为局部 value state
只有 profiler确认它每 logical call稳定 allocation才做。
当前 selection 主要保存:
snapshot identity
excluded mask
它跨 attempts使用,但只属于 InvokeUnaryWithRetryAsync 的局部生命周期。
候选:
private struct EndpointRetrySelectionState
{
object ? Snapshot ;
ulong ExcludedMask ;
}
然后内部选择调用明确使用 ref,防止 struct copy丢失 exclusion mutation。
硬约束
每个 attempt看到前一 attempt exclusion;
snapshot generation变化时 mask reset;
全部 endpoint excluded时现有 reset语义保持;
不把 ref逃逸到 async callback;
不把 mutable struct复制进 closure/boxed interface。
如果为了避免 copy需要大量侵入 API,且只省一个小对象,No-Go。
Step 4:Candidate B — 按 feature 拆 attempt state
这是优先于 pooling 的候选。
B1 Retry-only
当 _endpointAdmissionPolicy == null 时,retry真正需要的信息应审计为:
attempt elapsed
connection/endpoint identity(仅诊断/策略需要时)
completion reason
response observed
error code
不需要:
admission endpoint/token
admission grant/reject sequence
retry-after from admission
report flags
候选可以是更小的 internal retry attempt tracker,甚至部分 value state;但只要它注册为 PendingCall completion observer并跨 await,就要有稳定 owner。
B2 Admission-only
没有 retry时,主要职责是:
acquire token
remember endpoint
report outcome exactly once
不应为了未来 retry保存完整 retry state。
B3 Combined
retry + admission继续用完整 state,先不要为了统一强行复杂化。
设计要求
不同 state不能复制三套 terminal classification。建议抽一个纯函数 :
PendingCallCompletion -> EndpointOutcomeKind/ErrorCode
机制共享,mutable lifecycle不共享。
Step 5:Candidate C — lazy diagnostic identity
当前 EndpointId / ConnectionId 是 string reference字段。先 profiler确认是否导致 allocation;引用本身不代表 allocation。
如果 connection.Session.Id / endpoint diagnostics会在每 attempt创建 string,则可以考虑:
仅 retry policy真正需要 context时materialize;
或让 internal outcome暂存稳定 numeric/generation references,再在报告/日志边界格式化。
但公共 SharpLinkRetryContext / SharpLinkEndpointOutcome 契约若要求 string,不能静默删除。
没有 allocation stack证据不要动。
Step 6:Candidate D — bounded pooling(最后候选)
只有 B 拆分后仍存在显著 class allocation,并且 end-to-end收益空间足够,才考虑 pool。
Pool eligibility
对象必须:
internal;
用户拿不到引用;
owner唯一;
terminal明确;
reset字段可枚举;
retained references可完全清零;
有 hard retention cap。
Generation
必须有:
PendingCall observer注册时保存 generation/token。
callback进入时必须验证仍是同一 lifecycle;旧 generation只能 no-op,不能写新 state。
不允许
无界 ConcurrentBag;
无版本 intrusive lock-free stack;
Return时仍有 PendingCall callback可能发生;
pool保留 Method/Client/Endpoint/Exception/ALC强引用。
如果 generation让所有 callback API都变复杂,只为省很小 allocation,应撤回。
Retry correctness matrix
必须保持:
non-idempotent不 retry;
first success exactly one attempt;
unavailable/connection closed retry;
remote business error不误 retry;
response observed后按当前 policy判断;
max attempts exact;
exponential backoff;
jitter范围;
deadline阻止下一 attempt;
caller cancellation;
admission RetryAfter与policy delay取正确值;
endpoint exclusion在同 snapshot内生效;
topology generation变化正确 reset exclusion。
Admission / breaker correctness
policy TryAcquire throw -> FailedPrecondition;
negative RetryAfter被拒绝;
rejected endpoint不建立 PendingCall;
grant token只 report一次;
success report;
remote error report;
send failure report;
connection closed report;
deadline/cancel report;
CircuitBreaker Open/HalfOpen/Closed状态不变;
retry attempt Add experimental shared-memory transport #1 report完成后 #2才能独立 acquire/report;
local failure before Pending register也正确完成 admission lifecycle。
Deterministic races
至少:
Pending response与cancel同时;
response与disconnect;
response与deadline;
policy grant后、Pending register前 connection draining;
Pending terminal callback与 retry loop catch并发;
old callback发生在 state Return之后(pool candidate必须覆盖);
same endpoint generation replacement;
retry wait期间 Stop;
RetryAfter delay与deadline同刻。
所有 counter/report最终 exactly once。
正式 macro matrix
#122 后,同机交替 >=5 轮:
Default
Retry_FirstSuccess
Retry_OneFailure
Admission_Accept
Breaker_Closed
Breaker_Open
Retry+Admission_FirstSuccess
Retry+Admission_OneFailure
concurrency:1 / 32 / 128 / 512
TCP + SharedMemory。
记录:
QPS;
B/op;
CPU/op;
P99/P99.9;
attempts/call;
retry count;
policy acquire/report count;
failures。
Go / merge gates
Retry first-success
这是主要目标:
retry附加 allocation降低 >=30%;
或 full RPC CPU/QPS稳定改善 >=3%;
actual retry行为不回退 >3%。
Admission/Breaker accept path
feature附加 allocation降低 >=25%;
或 full RPC CPU/QPS稳定改善 >=3%。
Pool candidate额外门槛
如果不用 pool就已经获得大部分收益,不引入 pool。
pool必须至少比非pool candidate再带来:
=15% feature附加 allocation下降;或
=3% target workload CPU改善。
否则复杂度不值。
Default feature-off path B/op必须完全不增加。
Commit / rollback
1. bench: attribute client attempt state costs
2. experiment: make retry selection value-owned
3. experiment: split retry-only and admission attempt state
4. experiment: pool attempt state with generation // 最后才做
5. test: harden retry/admission lifecycle races
每项独立可回滚。
立即 revert,如果:
endpoint exclusion错误;
retry次数变化;
report double/missing;
breaker状态错误;
stale callback污染新 attempt;
deadline/cancel语义变化;
retained Client/Method/ALC reference;
target workload收益不足。
完成定义
目标
验证 Client 在启用 Retry、Endpoint Admission、Circuit Breaker 后产生的每逻辑调用 / 每 attempt 固定成本,并只对有证据的 state/ownership 做最小优化。
历史正式 evidence:
这些数字和当前对象模型高度相关,但不能直接把差值等同于某个 class。执行时必须重新读取当前
devHEAD、重新做 allocation stack,最终 macro 使用 #122。本 issue 要回答:
依赖
#81 可能删除/重命名重复 active counter;本 issue 不得提前把 counter 语义写死。可以先测 object/state,production merge 前必须 rebase/re-audit #81 结论。
当前
dev事实Retry path
InvokeUnaryWithRetryAsync当前会为一个 logical retry call 创建:然后每次 attempt:
即使第一 attempt 成功,这些状态也已经创建。
Endpoint admission / breaker without retry
默认 Unary core 在
_endpointAdmissionPolicy != null时也会创建:因此 AlwaysAcceptAdmission / ClosedCircuitBreaker 都会付 attempt-state 成本。
AttemptOutcomeState当前职责很多它同时保存:
这说明它不是可以直接删除的“DTO”。真正问题是:
第一性原理
1. 只为启用能力付成本
理想模型:
不应该因为一个大 class方便实现,让 retry-only 为 admission token/sequence/report 字段付完整对象成本。
2. Logical retry state 与 endpoint admission lease 是不同生命周期
Retry selection mask跨 attempt;Attempt outcome通常只对应一个 physical attempt;PendingCall completion observer可能比调用栈活得更久。
不要把它们为了“少对象”强行合成一个更难证明的大状态机。
3. Stale callback 是最重要风险
如果 attempt state进入 pool:
这是不可接受的 ABA/lifecycle bug。
因此 pooling 只有在 terminal ownership和 generation明确后才允许。
4. Retry first success 是最重要 control
正常健康系统最常见:
如果启用 retry 就固定 +500B,即使永远不真正 retry,也值得优化。
5. Policy semantics 高于 allocation
不能为了少对象改变:
Step 0:baseline
运行 Release build/unit/integration,特别覆盖 retry/circuit breaker/endpoint admission tests。
Step 1:建立 feature matrix microbenchmark
必须把能力拆开:
同一个 tiny Unary codec/transport fixture,避免 payload差异。
记录:
Step 2:allocation stack 必须拆出具体 owner
至少确认:
尤其要分清:
不能把用户 policy 自己分配的对象归到 Runtime。
Step 3:Candidate A —
EndpointRetrySelectionState改为局部 value state只有 profiler确认它每 logical call稳定 allocation才做。
当前 selection 主要保存:
它跨 attempts使用,但只属于
InvokeUnaryWithRetryAsync的局部生命周期。候选:
然后内部选择调用明确使用
ref,防止 struct copy丢失 exclusion mutation。硬约束
如果为了避免 copy需要大量侵入 API,且只省一个小对象,No-Go。
Step 4:Candidate B — 按 feature 拆 attempt state
这是优先于 pooling 的候选。
B1 Retry-only
当
_endpointAdmissionPolicy == null时,retry真正需要的信息应审计为:不需要:
候选可以是更小的 internal retry attempt tracker,甚至部分 value state;但只要它注册为 PendingCall completion observer并跨 await,就要有稳定 owner。
B2 Admission-only
没有 retry时,主要职责是:
不应为了未来 retry保存完整 retry state。
B3 Combined
retry + admission继续用完整 state,先不要为了统一强行复杂化。
设计要求
不同 state不能复制三套 terminal classification。建议抽一个纯函数:
机制共享,mutable lifecycle不共享。
Step 5:Candidate C — lazy diagnostic identity
当前
EndpointId/ConnectionId是 string reference字段。先 profiler确认是否导致 allocation;引用本身不代表 allocation。如果
connection.Session.Id/ endpoint diagnostics会在每 attempt创建 string,则可以考虑:但公共
SharpLinkRetryContext/SharpLinkEndpointOutcome契约若要求 string,不能静默删除。没有 allocation stack证据不要动。
Step 6:Candidate D — bounded pooling(最后候选)
只有 B 拆分后仍存在显著 class allocation,并且 end-to-end收益空间足够,才考虑 pool。
Pool eligibility
对象必须:
Generation
必须有:
PendingCall observer注册时保存 generation/token。
callback进入时必须验证仍是同一 lifecycle;旧 generation只能 no-op,不能写新 state。
不允许
ConcurrentBag;如果 generation让所有 callback API都变复杂,只为省很小 allocation,应撤回。
Retry correctness matrix
必须保持:
Admission / breaker correctness
TryAcquirethrow -> FailedPrecondition;Deterministic races
至少:
所有 counter/report最终 exactly once。
正式 macro matrix
#122 后,同机交替 >=5 轮:
concurrency:
1 / 32 / 128 / 512TCP + SharedMemory。
记录:
Go / merge gates
Retry first-success
这是主要目标:
Admission/Breaker accept path
Pool candidate额外门槛
如果不用 pool就已经获得大部分收益,不引入 pool。
pool必须至少比非pool candidate再带来:
否则复杂度不值。
Default feature-off path B/op必须完全不增加。
Commit / rollback
每项独立可回滚。
立即 revert,如果:
完成定义
EndpointRetrySelectionState与AttemptOutcomeState独立归因。