总跟踪:#262
架构背景:#67
分析与实施基线:dev
模块定位:本 issue 是 Runtime Dynamic Configuration 的第二类实现模板,代表 immutable program + stateful runtime state + retire/drain 。
Use case
SharpLink 当前 Server Admission Control 只能在 Builder 阶段通过 UseAdmissionControl(...) 配置,Server Build 后 controller、rule、queue、rate limiter、partition 等状态固定到实例生命周期结束。
希望支持运行时:
启用 Admission;
禁用 Admission;
调整全局 / Contract / Method concurrency;
调整 queue count / bytes / delay / OneWay queue policy;
调整 token bucket / fixed window / sliding window 配置;
增加、删除、替换 Contract / Method rule;
调整 partition 容量 / idle timeout;
必要时替换 partition selector / rate algorithm;
在不重启 Server、不杀死正常 in-flight RPC 的情况下完成上述更新。
同时必须保护性能:
Admission disabled 时继续接近现有 direct fast path;
request path 不加全局 lock;
update 在 control path 构建、校验、reconcile,最后 atomic publish;
不能为了“动态”破坏现有 bounded queue、permit accounting、OneWay 行为和 pre-admission compressed payload 逻辑。
本 issue 不是 简单把 _admissionController 改成可写字段。Admission 是 stateful subsystem,若直接 new controller -> swap,会产生真实资源上限错误。
当前实现与关键路径
配置入口
重点文件:
src/SharpLink.Server/SharpLinkServerBuilder.cs
src/SharpLink.Server/ServerBuildPlan.cs
src/SharpLink.Server/Admission/SharpLinkAdmissionControlOptions.cs
当前配置能力包括:
Global rule;
Contract rule;
Method rule;
Concurrency;
Token Bucket;
Fixed Window;
Sliding Window;
bounded queue:MaxQueuedCalls / MaxQueuedBytes / MaxQueueDelay;
QueueOneWayCalls;
Partition selector / MaxPartitions / IdleTimeout。
Builder / BuildPlan 仍应保留为初始配置编译入口 ,本 issue 不删除 Build-time validation。
RequestLoop:Admission 同时决定“是否预解压”
重点文件:
src/SharpLink.Server/SharpLinkServer.RequestLoop.cs
src/SharpLink.Server/SharpLinkServer.PreAdmissionStreams.cs
当前 Request frame 在读取时有重要分支:
if ( header . Type == ProtocolV2FrameType . Request &&
_admissionController is not null )
{
session . ValidateInboundPayloadEnvelope ( ... ) ;
}
else
{
payload = session . DecodeInboundPayload ( ... ) ;
}
也就是说 _admissionController != null 不只是“要不要限流”的开关,它还决定:
Admission enabled
↓
先验证 envelope
保留压缩 payload
↓
Admission acquire
↓
通过后再 decode
因此动态化后必须在 Request frame 处理入口 capture 一次 admission generation,并把它一路带到 dispatch 。
绝不能:
RequestLoop 看到 enabled -> 不 decode
↓
配置线程 disable
↓
Dispatch 再读全局字段看到 disabled -> 不做 Admission / 不做 deferred decode
这种实现会让一个 Request 一半走 enabled 路径、一半走 disabled 路径。
Admission dispatch
重点文件:
src/SharpLink.Server/SharpLinkServer.AdmissionDispatch.cs
当前 dispatch 会:
创建 Admission Context;
调用 _admissionController.AcquireAsync(...);
OneWay 根据 QueueOneWayCalls 决定是否可排队;
async admission 时复制/保留 payload 并等待;
admission success 后把 AdmissionLease attach 到 call state;
admission enabled 时在后续阶段执行 deferred DecodeInboundPayload(...);
rejection 返回 ResourceExhausted / structured reason;
OneWay reject/drop 做独立 telemetry/logging。
动态化后这些步骤必须使用同一个 captured generation ,不能在 await 前后重新读 server 当前配置。
Controller 当前把 policy 与 state 混在一起
重点文件:
src/SharpLink.Server/Admission/SharpLinkAdmissionController.cs
当前 Controller 同时拥有:
Global AdmissionRuleRuntime;
Contract / Method FrozenDictionary rule runtime;
Partition pool;
queue limits;
_queuedCalls / _queuedBytes;
_activePermits;
draining CTS;
queue/permit drained signals;
RateLimiter instances。
AdmissionRuleRuntime.Create(...) 当前直接构造 BCL:
ConcurrencyLimiter;
TokenBucketRateLimiter;
FixedWindowRateLimiter;
SlidingWindowRateLimiter。
这些 limiter 的构造配置固定,因此不能简单原地改参数。
Lease / drain
当前 AdmissionLease:
持有 controller owner;
持有一个或多个 RateLimitLease;
持有 partition lease;
create 时增加 active permit accounting;
Dispose 时释放 limiter/partition,再减少 active permit accounting。
当前 SharpLinkAdmissionController.DisposeAsync() 会:
StopAccepting();
等 queue drain;
等 active permits drain;
Dispose rules / partitions。
这套机制是 server shutdown 语义,可复用其思想,但不能直接用于普通配置 disable/update ,因为 shutdown-style StopAccepting() 会取消/拒绝 queued calls,而 runtime 配置切换默认不应杀掉已有业务调用。
已有测试与 benchmark
优先扩展:
test/SharpLink.UnitTests/Server/AdmissionControlTests.cs
test/SharpLink.Benchmarks/AdmissionBenchmarks.cs
doc/admission-control.md
现有 tests 已覆盖:
配置 validation;
queue accounting;
immediate admission allocation;
partition capacity / reclaim;
frozen partition config;
token/fixed/sliding rate reject;
composite queue / retained rate lease;
dispose / lease accounting 等。
现有 benchmark 已有:
end-to-end Disabled;
end-to-end ImmediatePermit;
controller ImmediatePermit;
ImmediateRejection;
QueueAndRelease。
这些必须作为动态化性能基线继续保留。
为什么不能 new Controller + atomic swap
Concurrency shrink 会真实超限
假设:
old limit = 100
old active = 80
更新为:
如果:
var candidate = SharpLinkAdmissionController . Create ( newOptions , ... ) ;
Volatile . Write ( ref _admissionController , candidate ) ;
new controller 的 active count 是 0,因此还能接收 50 个:
old active = 80
new active = 50
actual active = 130
更新目标是 100 -> 50,结果瞬间变成 130,违反 Admission 的核心资源边界。
Rate limiter replacement 会送免费 burst
例如 token bucket 已经消耗到只剩少量 token,直接 new 一个新 bucket 通常从完整 capacity 开始,会因为“改配置”额外释放一批请求。
Fixed / Sliding Window 同样存在窗口状态重置问题。
Partition state 会丢失
直接 replacement 会丢掉:
active partition;
idle/reclaim 状态;
partition concurrency/rate state。
Queue state 会割裂
old queue 与 new controller queue 分开计数,会导致:
MaxQueuedCalls 实际叠加;
MaxQueuedBytes 实际叠加;
queued request 与新 policy 的语义不清。
因此本 issue 必须实现 policy/program 与 runtime state 分离 。
核心目标模型
推荐概念结构:
SharpLinkServer
│
├── volatile AdmissionProgram? _admissionProgram
│ null = disabled
│
└── AdmissionStateKernel
├── reusable concurrency states
├── rate states / generations
├── queue accounting
├── partition generations
└── retired state reclamation
AdmissionProgram
AdmissionProgram 是 immutable publication unit,负责:
当前 Global / Contract / Method route;
immutable rule bindings;
queue policy snapshot;
OneWay queue policy;
partition selector generation binding;
预解析的 generated contract / method IDs;
指向 runtime state objects 的稳定引用。
Program 构造发生在 control path。
AdmissionStateKernel
Kernel 是 Server instance scoped,负责真正的 mutable runtime accounting:
concurrency active count;
queued calls / bytes;
rate/window state;
partition state;
retired generation 生命周期。
不要做 process-global registry。
Rule identity
状态复用必须基于明确 identity,不能按 object reference 猜。
建议至少包含:
scope kind: Global / Contract / Method / Partition
contractId
methodId (if any)
limiter kind: Concurrency / TokenBucket / FixedWindow / SlidingWindow
partition generation identity (if applicable)
Generated stable IDs 优先,不引入 request-path reflection。
生效语义
Enable
disabled (null)
↓
control path build + validate + resolve + reconcile
↓
atomic publish AdmissionProgram N
↓
new Request 使用 N
不得在 publish 前让 request path 看见半构造 rule/state。
Disable
AdmissionProgram N
↓
atomic exchange current => null
↓
new Request bypass Admission
existing queued / active request keep N/state
↓
N retire when safe
默认不取消 :
已获得 permit 的调用;
已进入 Admission queue 的调用。
Disable 的语义是“后续新请求不再接受 Admission 限制”,不是“强制取消旧请求”。
Replace / update
Program N
↓
build candidate N+1
↓
reuse compatible state
create new state for new/structurally changed rules
validate transition
↓
atomic publish N+1
↓
retire N when no longer referenced
Existing call consistency
一个 Request 从 RequestLoop 决定 pre-admission decode mode 开始,就必须绑定 admission generation,直到:
reject;或
queued wait 结束并 reject;或
acquire 成功,lease 最终随 call terminal release。
不能在 async continuation / stream / decode 阶段重新读取 _admissionProgram 决定该 Request 的逻辑。
Generation lifetime:必须解决 use-after-dispose
因为动态更新会 retire program,而 RequestLoop 可能刚读取 old program,因此不能:
var old = Interlocked . Exchange ( ref _admissionProgram , next ) ;
old . Dispose ( ) ;
可能存在:
request thread: read old
config thread: swap + Dispose(old)
request thread: old.Acquire(...)
推荐引入轻量 generation-use lease / refcount:
Volatile.Read(current)
↓
TryAcquireUse()
↓
program cannot be reclaimed
↓
request admission / queue / call
↓
ReleaseUse() / transfer to AdmissionLease
要求:
disabled (null) path 不做 Interlocked refcount;
enabled path 的 generation lifetime 成本必须 benchmark;
queued request 必须持有 generation use;
acquire success 后 generation use 可由 AdmissionLease 接管,直到 service call terminal;
reject / parse failure / deadline / unknown service 等 early exit 必须释放;
program retire 后禁止新的 TryAcquireUse,但已有 use 可继续;
program/state 只能在 retired + use count zero 后 reclaim。
如果实现者使用等价的 epoch/hazard/RCU 方案也可以,但必须证明:
无 use-after-dispose;
retired generations 最终可回收;
不允许“为了简单”把所有历史 controller 永久留到 server dispose,造成更新次数无界增长。
动态模块现有 lease/drain 测试模式可作为参考:
test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs
具体实施步骤
Phase 0 — 先补关键不变量测试
在大规模重构前先写 deterministic tests,至少先锁住:
current concurrency accounting;
queue accounting;
rate permit retention;
dispose exactly-once;
pre-admission compressed request 行为。
现有 test 已有部分,缺失的先补。
Phase 1 — 把 Request frame 的 Admission generation capture 前移
修改候选:
src/SharpLink.Server/SharpLinkServer.cs
src/SharpLink.Server/SharpLinkServer.RequestLoop.cs
src/SharpLink.Server/SharpLinkServer.AdmissionDispatch.cs
不要先做 limit resize,先把 correctness plumbing 做对。
RequestLoop 在处理 Request frame 时:
原子读取当前 Admission Program;
若 non-null,获得安全 generation use;
使用这个 captured generation 决定 ValidateEnvelope vs DecodeInboundPayload ;
将 generation/use 传入 DispatchOneWayRpc / DispatchRpcAsync;
dispatch 及其所有 async continuation 只使用传入 generation。
概念:
var admission = TryCaptureAdmissionProgram ( ) ;
if ( header . Type == Request && admission . IsEnabled )
ValidateInboundPayloadEnvelope ( ... ) ;
else
DecodeInboundPayload ( ... ) ;
DispatchRpcAsync ( ... , admission ) ;
注意所有 early exit 的 ownership transfer,避免 lease leak。
Phase 2 — 清除 dispatch 内对 server 当前 admission field 的二次读取
当前 AdmissionDispatch 中类似:
if ( _admissionController is not null && ! admissionGranted )
以及后面:
if ( _admissionController is not null )
payload = session . DecodeInboundPayload ( ... ) ;
动态化后这些必须改为“本 Request captured state”:
capturedAdmission.IsEnabled
capturedAdmission.Program
admissionGranted / attached lease
尤其 async queue resume 以后,global current program 可能已经 disable/replace,不能影响 old Request 的 deferred decode。
必须审计:
Unary/response-bearing dispatch;
OneWay dispatch;
ClientStreaming / Duplex pre-admission streams;
admission wait continuation;
reject / deadline / cancellation / module drain 分支。
Phase 3 — 拆 AdmissionProgram 与 state
重构 SharpLinkAdmissionController,不要求一次改名,但最终职责必须分开。
建议拆分候选:
AdmissionProgram — immutable route/config binding;
AdmissionStateKernel — server-owned state registry / queue accounting;
AdmissionRuleState — per-scope limiter state;
AdmissionQueuePolicy — immutable current queue policy;
AdmissionPartitionGeneration — partition selector/pool generation;
generation retirement/reclamation helper。
不要为了目录形式机械拆 class;核心验收是 policy 更新时真实 state 不丢。
Phase 4 — 动态 Concurrency:优先做成真正 live-resizable
这是第一项必须完整支持的 stateful update。
语义:
limit 100
active 80
↓ update to 50
active remains 80
new calls do NOT enter
↓
80 -> 79 -> ... -> 50
still no new permit
↓
49
new admission can resume
禁止杀死已有 80 个调用。
Increase:
应立即允许更多新 admission。
实现可以是自定义 ResizableConcurrencyState / limiter abstraction,重点是 active count 必须跨 program generation 复用。
如果继续包装 BCL ConcurrencyLimiter,必须证明 resize 不通过“fresh limiter with active=0”破坏 bound;不能只换 options。
并发 acquire/release/update 必须无 underflow、无超过 current effective bound 的新 admission。
Phase 5 — Queue policy 动态更新
动态支持:
MaxQueuedCalls;
MaxQueuedBytes;
MaxQueueDelay;
QueueOneWayCalls。
语义:
queue count/bytes shrink
假设:
queued = 600
new MaxQueuedCalls = 100
不要取消已有 500 个。
新 waiter 应拒绝,直到 accounting 回到新 limit 以下。
queue count/bytes increase
更新后新 waiter 立即使用新值。
MaxQueueDelay
建议 enqueue 时 capture:
A enqueue under 2s
update => 500ms
A remains 2s budget
new B uses 500ms
不要让一个已经等待的 request 因配置线程修改某字段而发生不可预测 deadline 跳变。
QueueOneWayCalls
对新的 OneWay request 生效;已经进入 queue 的 OneWay 不因关闭该选项被强制 drop。
Queue accounting 最好继续由 server-owned shared state 统一维护,不要按 program 拆出多个独立 _queuedCalls,否则多个 generation overlap 时总 queue 会超出目标。
Phase 6 — Dynamic rule add/remove/replace
Program compile 时构建 immutable route:
Global
ContractId -> binding
(ContractId, MethodId) -> binding
Partition -> generation
推荐在 update path 预解析/预编译,不要在 request path reflection 查 type/method name。
更新规则:
unchanged rule identity -> reuse compatible runtime state;
concurrency numeric change -> update same state;
new rule -> create state;
removed rule -> new calls不再看到;old generation 继续完成;
structurally changed limiter kind -> new state generation,old drain。
Runtime compile 必须复用当前 Build-time validation/ID resolution 原则。若基于 contract type/method name 的 rule 需要 resolve,应 against 当前可用 generated manifest/service snapshot;不要引入 reflection fallback。
Phase 7 — Rate limiter 更新
支持:
Token Bucket;
Fixed Window;
Sliding Window。
这是比 concurrency 更高风险的部分。
硬性不变量
配置更新不得凭空产生免费 burst。
例如 old token bucket 已耗尽,update 后不能因为 new BCL limiter 初始 full capacity 而立刻多放一整桶请求。
可接受实现路线 A:state-preserving dynamic implementation
实现本项目自己的 narrow admission rate state,保存:
current token/window counters;
monotonic timestamps / segment state;
atomic/locked policy parameters;
同算法参数变化时 reconcile 当前状态。
优点:语义最好。风险:实现 fixed/sliding window correctness 较复杂,必须大量 fake-time 测试。
可接受实现路线 B:conservative generation transition
继续使用 BCL RateLimiter,但配置变化视为 structural generation transition。
要求:
新 generation 不得以 full capacity 造成额外 burst;
必须采用保守初始化/过渡策略;
old queued/retained rate lease 安全完成;
transition 行为必须 documented + deterministic tests;
可以短暂过度限流,但不能短暂突破新/旧约束造成资源放大。
禁止 :直接 new TokenBucketRateLimiter(newOptions) 后立即 publish fresh full bucket,测试只检查“最终能请求”而忽略 burst。
Algorithm change
例如:
TokenBucket -> SlidingWindow
视为 structural change:
new calls 使用新 state generation;
old queued/lease 使用旧 generation;
old safe drain 后 reclaim;
不尝试把 token 数机械解释成 sliding segments。
Phase 8 — Partition 动态化
区分两类:
同 selector 下参数更新
MaxPartitions;
IdleTimeout;
partition concurrency/rate 参数。
可以在保持 partition namespace 的前提下 reconcile/update。
Shrink MaxPartitions 时不要驱逐 active partition;按 idle reclaim 逐步回落。
selector replacement
例如:
partition by tenant-id
↓
partition by user-id
这是新的 namespace,必须创建新 AdmissionPartitionGeneration。
不要迁移 old dictionary entry 到新 selector。
new requests -> new partition generation
old requests/leases -> old generation
old generation drain + idle state reclaim
Phase 9 — Enable / Disable / Re-enable
Disable:
优先用 null 表示 disabled,不使用 NoopAdmissionController。
新 request:直接 bypass。
old request:继续 old generation。
不要调用 shutdown StopAccepting() 取消旧 queue。
Re-enable:
build/validate candidate;
compatible state 若仍有 old active/queued,应复用,避免 concurrency accounting 分裂;
fully drained compatible state 可复用或重建,但行为必须等价且无 burst;
atomic publish。
Phase 10 — Server Stop / Dispose 整合
Server shutdown 与 runtime disable 是不同语义。
Shutdown:
先禁止 server 接受新 RPC;
seal admission update control path;
retire current program;
对所有 live/retired admission generations 进入 shutdown cancellation/stop accepting;
queued waiters 按现有 server shutdown semantics 终止;
active permit 随业务调用 terminal release;
等待所有 admission-owned waiter/lease/state drain;
Dispose limiter/partition/timer exactly once。
必须避免:
Stop 正在 drain old generation
↓
Update 线程又 publish new admission program
Update 与 Stop 需要同一个 lifecycle barrier/control gate 语义。
Runtime control API
最终 public naming 可在 PR review 决定,但底层至少需要表达:
Enable/Configure
Update
Disable
候选形式:
server . UpdateAdmissionControl ( options => { ... } ) ;
server . DisableAdmissionControl ( ) ;
或独立 control handle。
无论 API 形式如何:
SharpLinkAdmissionControlOptions 作为 candidate builder 使用;
不允许 runtime request path 长期读取 mutable options object;
update 完整 clone/build/validate;
validation failure 不改变 current program/state;
update writer 串行化,避免 read-modify-publish lost update;
lifecycle stopped/disposed 后禁止再次 publish;
Builder UseAdmissionControl 仍负责 initial generation。
不要在本 issue 顺便统一所有 Runtime Configuration API。
热路径设计要求
Disabled path
目标:
Request frame
↓
1 atomic/volatile program pointer read
↓
null branch
↓
现有 direct decode + dispatch
要求:
Enabled immediate-permit path
动态支持不得让每个 limiter slot 都重新读取 global config。
优先:
capture program once;
precompiled/immutable route;
reuse runtime states;
immediate success 尽量同步完成;
只有真实 queue/async wait 才创建较重 waiter state。
现有 unit test:
ImmediateAdmissionShouldNotAllocateThreeTransientArraysPerCall
必须保留且建议进一步收紧/优化;动态化不得明显增加 immediate admission allocation。
Precompiled route 优化(建议)
当前 scope 最大通常是:
Global
Contract
Method
Partition
每层最多 concurrency + rate。
Program build/update path 可以预计算 static Global/Contract/Method execution plan,避免每个 request 重新拼 immutable route/临时集合。
Partition 因 key 动态解析,可最后追加。
这个优化不是为了“代码漂亮”,而是为了抵消 enabled generation capture/lifecycle 成本。
测试实施手册
主文件:
test/SharpLink.UnitTests/Server/AdmissionControlTests.cs
建议新增:
test/SharpLink.UnitTests/Server/DynamicAdmissionControlTests.cs
test/SharpLink.IntegrationTests/DynamicAdmissionIntegrationTests.cs
避免让已有单文件无限膨胀,但复用现有 helper/fake rate limiter。
A. Enable / Disable
B. Concurrency resize — 必须 deterministic
Increase:
limit=1
1 active + 1 rejected/queued
update=2
second can acquire
Shrink:
limit=3
3 active
update=1
new requests cannot acquire
release -> active 2, still cannot
release -> active 1, still no extra beyond bound
release/transition -> capacity resumes correctly
必须覆盖并发 acquire/release/update stress,并断言 max observed active 不因 generation replacement超过语义上允许值。
C. Disable / Re-enable 与 old active overlap
关键回归:
limit=2
2 old active
Disable
Enable same limit=2
如果 old state 尚未 drain,不能让 new generation 再接 2 个导致 admission accounting 被拆成 4。
测试必须能发现这种 naive-controller-swap bug。
D. Queue policy update
E. Rate update
三个算法分别测试:
所有时间相关测试优先使用项目统一 TimeProvider / fake time,不要依赖 Task.Delay + wall clock 猜窗口。
F. Rule routing
G. Partition
H. RequestLoop generation consistency
这是与普通 limiter 单测不同的重点集成测试。
必须覆盖:
Request frame capture old enabled generation;
frame 已选择 pre-admission(未 decode);
配置线程 disable/replace;
old Request 仍按 old generation acquire + deferred decode;
新 Request 使用新 generation / disabled path。
如果无法直接 deterministic 卡在 frame capture 点,可使用测试 transport/barrier 构造;不要只用高概率 stress 代替核心 deterministic test。
I. Compression / pre-admission payload
Admission enable/disable 会改变 Request payload decode 时机,因此至少测试:
J. Cancellation / Deadline / Stop
K. Stress
运行:
many RPC workers
+
config updater cycles:
Disabled
Global concurrency 64
Global concurrency 8
Method rule add/remove
queue shrink/grow
rate policy change
Disabled
持续检查:
active count invariants;
queue count/bytes invariants;
no deadlock;
no ObjectDisposedException from using retired limiter;
no memory/state generation unbounded growth;
no protocol/decompression failure;
no unexpected connection close。
Benchmark / 性能门禁
主文件:
test/SharpLink.Benchmarks/AdmissionBenchmarks.cs
必须扩展现有 benchmark,而不是只跑 micro-controller benchmark。
End-to-end RPC scenarios
至少:
Disabled — dynamic-capable server but current program null;
ImmediatePermit — global concurrency ample;
ImmediatePermitAfterResize — 多次 update 后 steady state;
ImmediateReject;
MethodRuleImmediatePermit;
如 partition 是本 PR 范围,PartitionImmediatePermit。
Controller/state microbench
至少:
concurrency acquire/release;
concurrency after resize;
immediate rejection;
queue/release;
rate immediate permit/reject(若 rate dynamic 本 PR 完成)。
需要报告
Mean / throughput;
B/op;
ThreadingDiagnoser / lock contention;
before/after dev baseline。
性能要求
Disabled:
Enabled immediate permit:
Update 本身不是 request-rate 操作,可以分配/加 control lock,但应有 stress 证明 publication pause 不扩散到 request threads。
验证命令
基础:
dotnet build Sharplink.slnx -c Release
dotnet test --project test/SharpLink.UnitTests/SharpLink.UnitTests.csproj -c Release
dotnet test --project test/SharpLink.Generator.Tests/SharpLink.Generator.Tests.csproj -c Release
dotnet run --project test/SharpLink.IntegrationTests/SharpLink.IntegrationTests.csproj -c Release -- --timeout 120s
按影响范围补:
Admission 单测 filter / dynamic admission integration;
Streaming integration;
Compression integration;
Chaos tests(update/stop/cancel);
RuntimeAssemblyIntegrationTests / MultiCluster tests(若 route 解析和 dynamic modules 交叉);
NativeAOT smoke(若 public API / generated route 解析发生变化)。
BenchmarkDotNet:至少运行 AdmissionRpcBenchmarks、AdmissionControllerBenchmarks 及新增 dynamic admission benchmark,并在 PR 附 before/after 数据。
风险清单
R1 — Request decode generation 混用
最高 correctness 风险。RequestLoop 与 AdmissionDispatch 必须共享同一个 capture。
R2 — Concurrency 更新反而超限
任何 fresh controller active=0 的方案都会有这个风险。
R3 — Rate update 免费 burst
不能因为配置变化重置 token/window quota。
R4 — Retired state use-after-dispose
Atomic swap 不等于安全 reclaim。必须 generation use/drain 或等价机制。
R5 — 历史 generation 永久保留
为了避免 use-after-dispose 而永不回收也不接受;更新次数不能线性永久增长内存。
R6 — Queue generation 分裂
多个 controller 各自 queue accounting 会破坏 global bound。
R7 — Disable 错用 shutdown semantics
普通 Disable 不应该调用当前 shutdown-style StopAccepting() 把 queued calls 变成 Unavailable。
R8 — Partition selector 状态错误迁移
不同 selector namespace 不能复用 old partition dictionary。
R9 — Stop 与 update 竞态
Stop seal 后绝不能 publish 新 generation。
R10 — OneWay 行为回归
QueueOneWayCalls、drop、rejected metrics、stream draining 都要单独验证。
R11 — Pre-admission stream/buffer owner leak
queued request 会 retain payload/stream resources,generation update 不得导致 decoded owner、copied payload、stream reservation 丢失或 double release。
R12 — 把 Connection Admission 混进来
SharpLinkConnectionAdmissionOptions / connection-level admission 是独立生命周期与入口。本 issue 默认只处理 RPC Server Admission Control ;connection admission 动态化另开 issue,避免扩大状态空间。
明确禁止的实现
不把 SharpLinkAdmissionControlOptions 直接变成线程安全 mutable runtime object。
不在每个 Request 上 lock(config)。
不用 ConcurrentDictionary 全面替换 Frozen route 只为了支持 update。
不通过 new SharpLinkAdmissionController 简单 replacement 处理 concurrency resize。
不允许 rate config update 重新获得完整 quota burst。
不在每次 limiter slot lookup 时读 global current program。
不在 async queue continuation 恢复时重新选择 current generation。
不在 ordinary Disable 时取消已有业务调用/queue。
不为了回收简单而在 update thread 直接 Dispose old controller/state。
不用 process-global static registry 保存 admission generations。
Out of scope
Client Endpoint Admission / Circuit Breaker 动态化,另属 [Runtime Configuration] 动态配置 / Hot Reload 长期跟踪 #262 子模块。
Server connection-level admission 动态化。
MaxConcurrentCallsPerServer / flow-control 全部统一进本 Admission 模块;可复用设计但另议。
Protocol / handshake / compression provider renegotiation。
最终统一 Runtime Configuration public API / Hosting OptionsMonitor integration。
建议提交拆分
本 issue 可以由一个聚焦 PR 完成,也可在同一 issue 下拆成顺序 PR;若拆 PR,每一步必须保持可运行,不允许长期保留两套互相竞争的 admission path。
建议 commits / PR stages:
test: lock admission generation and accounting invariants
refactor: capture admission generation at request frame boundary
refactor: split admission program from runtime state
feat: add runtime enable disable and concurrency resize
feat: add dynamic queue and rule updates
feat: add safe rate and partition transitions
test: add dynamic admission integration and stress coverage
perf: extend admission benchmarks
docs: document runtime admission semantics
如果 rate/partition 复杂度需要独立 PR,可在本 issue 下继续,不要把未完成部分静默标 Done。
Definition of Done
Checks
Use case
SharpLink 当前 Server Admission Control 只能在 Builder 阶段通过
UseAdmissionControl(...)配置,Server Build 后 controller、rule、queue、rate limiter、partition 等状态固定到实例生命周期结束。希望支持运行时:
同时必须保护性能:
本 issue 不是简单把
_admissionController改成可写字段。Admission 是 stateful subsystem,若直接new controller -> swap,会产生真实资源上限错误。当前实现与关键路径
配置入口
重点文件:
src/SharpLink.Server/SharpLinkServerBuilder.cssrc/SharpLink.Server/ServerBuildPlan.cssrc/SharpLink.Server/Admission/SharpLinkAdmissionControlOptions.cs当前配置能力包括:
MaxQueuedCalls/MaxQueuedBytes/MaxQueueDelay;QueueOneWayCalls;MaxPartitions/IdleTimeout。Builder / BuildPlan 仍应保留为初始配置编译入口,本 issue 不删除 Build-time validation。
RequestLoop:Admission 同时决定“是否预解压”
重点文件:
src/SharpLink.Server/SharpLinkServer.RequestLoop.cssrc/SharpLink.Server/SharpLinkServer.PreAdmissionStreams.cs当前 Request frame 在读取时有重要分支:
也就是说
_admissionController != null不只是“要不要限流”的开关,它还决定:因此动态化后必须在 Request frame 处理入口 capture 一次 admission generation,并把它一路带到 dispatch。
绝不能:
这种实现会让一个 Request 一半走 enabled 路径、一半走 disabled 路径。
Admission dispatch
重点文件:
src/SharpLink.Server/SharpLinkServer.AdmissionDispatch.cs当前 dispatch 会:
_admissionController.AcquireAsync(...);QueueOneWayCalls决定是否可排队;AdmissionLeaseattach 到 call state;DecodeInboundPayload(...);ResourceExhausted/ structured reason;动态化后这些步骤必须使用同一个 captured generation,不能在 await 前后重新读 server 当前配置。
Controller 当前把 policy 与 state 混在一起
重点文件:
src/SharpLink.Server/Admission/SharpLinkAdmissionController.cs当前 Controller 同时拥有:
AdmissionRuleRuntime;_queuedCalls/_queuedBytes;_activePermits;AdmissionRuleRuntime.Create(...)当前直接构造 BCL:ConcurrencyLimiter;TokenBucketRateLimiter;FixedWindowRateLimiter;SlidingWindowRateLimiter。这些 limiter 的构造配置固定,因此不能简单原地改参数。
Lease / drain
当前
AdmissionLease:RateLimitLease;当前
SharpLinkAdmissionController.DisposeAsync()会:StopAccepting();这套机制是 server shutdown 语义,可复用其思想,但不能直接用于普通配置 disable/update,因为 shutdown-style
StopAccepting()会取消/拒绝 queued calls,而 runtime 配置切换默认不应杀掉已有业务调用。已有测试与 benchmark
优先扩展:
test/SharpLink.UnitTests/Server/AdmissionControlTests.cstest/SharpLink.Benchmarks/AdmissionBenchmarks.csdoc/admission-control.md现有 tests 已覆盖:
现有 benchmark 已有:
Disabled;ImmediatePermit;ImmediatePermit;ImmediateRejection;QueueAndRelease。这些必须作为动态化性能基线继续保留。
为什么不能
new Controller + atomic swapConcurrency shrink 会真实超限
假设:
更新为:
如果:
new controller 的 active count 是 0,因此还能接收 50 个:
更新目标是
100 -> 50,结果瞬间变成130,违反 Admission 的核心资源边界。Rate limiter replacement 会送免费 burst
例如 token bucket 已经消耗到只剩少量 token,直接 new 一个新 bucket 通常从完整 capacity 开始,会因为“改配置”额外释放一批请求。
Fixed / Sliding Window 同样存在窗口状态重置问题。
Partition state 会丢失
直接 replacement 会丢掉:
Queue state 会割裂
old queue 与 new controller queue 分开计数,会导致:
因此本 issue 必须实现 policy/program 与 runtime state 分离。
核心目标模型
推荐概念结构:
AdmissionProgram
AdmissionProgram是 immutable publication unit,负责:Program 构造发生在 control path。
AdmissionStateKernel
Kernel 是 Server instance scoped,负责真正的 mutable runtime accounting:
不要做 process-global registry。
Rule identity
状态复用必须基于明确 identity,不能按 object reference 猜。
建议至少包含:
Generated stable IDs 优先,不引入 request-path reflection。
生效语义
Enable
不得在 publish 前让 request path 看见半构造 rule/state。
Disable
默认不取消:
Disable 的语义是“后续新请求不再接受 Admission 限制”,不是“强制取消旧请求”。
Replace / update
Existing call consistency
一个 Request 从
RequestLoop决定 pre-admission decode mode 开始,就必须绑定 admission generation,直到:不能在 async continuation / stream / decode 阶段重新读取
_admissionProgram决定该 Request 的逻辑。Generation lifetime:必须解决 use-after-dispose
因为动态更新会 retire program,而 RequestLoop 可能刚读取 old program,因此不能:
可能存在:
推荐引入轻量 generation-use lease / refcount:
要求:
null) path 不做 Interlocked refcount;AdmissionLease接管,直到 service call terminal;TryAcquireUse,但已有 use 可继续;如果实现者使用等价的 epoch/hazard/RCU 方案也可以,但必须证明:
动态模块现有 lease/drain 测试模式可作为参考:
test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs具体实施步骤
Phase 0 — 先补关键不变量测试
在大规模重构前先写 deterministic tests,至少先锁住:
现有 test 已有部分,缺失的先补。
Phase 1 — 把 Request frame 的 Admission generation capture 前移
修改候选:
src/SharpLink.Server/SharpLinkServer.cssrc/SharpLink.Server/SharpLinkServer.RequestLoop.cssrc/SharpLink.Server/SharpLinkServer.AdmissionDispatch.cs不要先做 limit resize,先把 correctness plumbing 做对。
RequestLoop 在处理
Requestframe 时:DispatchOneWayRpc/DispatchRpcAsync;概念:
注意所有 early exit 的 ownership transfer,避免 lease leak。
Phase 2 — 清除 dispatch 内对 server 当前 admission field 的二次读取
当前
AdmissionDispatch中类似:以及后面:
动态化后这些必须改为“本 Request captured state”:
尤其 async queue resume 以后,global current program 可能已经 disable/replace,不能影响 old Request 的 deferred decode。
必须审计:
Phase 3 — 拆
AdmissionProgram与 state重构
SharpLinkAdmissionController,不要求一次改名,但最终职责必须分开。建议拆分候选:
AdmissionProgram— immutable route/config binding;AdmissionStateKernel— server-owned state registry / queue accounting;AdmissionRuleState— per-scope limiter state;AdmissionQueuePolicy— immutable current queue policy;AdmissionPartitionGeneration— partition selector/pool generation;不要为了目录形式机械拆 class;核心验收是 policy 更新时真实 state 不丢。
Phase 4 — 动态 Concurrency:优先做成真正 live-resizable
这是第一项必须完整支持的 stateful update。
语义:
禁止杀死已有 80 个调用。
Increase:
应立即允许更多新 admission。
实现可以是自定义
ResizableConcurrencyState/ limiter abstraction,重点是 active count 必须跨 program generation 复用。如果继续包装 BCL
ConcurrencyLimiter,必须证明 resize 不通过“fresh limiter with active=0”破坏 bound;不能只换 options。并发 acquire/release/update 必须无 underflow、无超过 current effective bound 的新 admission。
Phase 5 — Queue policy 动态更新
动态支持:
MaxQueuedCalls;MaxQueuedBytes;MaxQueueDelay;QueueOneWayCalls。语义:
queue count/bytes shrink
假设:
不要取消已有 500 个。
新 waiter 应拒绝,直到 accounting 回到新 limit 以下。
queue count/bytes increase
更新后新 waiter 立即使用新值。
MaxQueueDelay
建议 enqueue 时 capture:
不要让一个已经等待的 request 因配置线程修改某字段而发生不可预测 deadline 跳变。
QueueOneWayCalls
对新的 OneWay request 生效;已经进入 queue 的 OneWay 不因关闭该选项被强制 drop。
Queue accounting 最好继续由 server-owned shared state 统一维护,不要按 program 拆出多个独立
_queuedCalls,否则多个 generation overlap 时总 queue 会超出目标。Phase 6 — Dynamic rule add/remove/replace
Program compile 时构建 immutable route:
推荐在 update path 预解析/预编译,不要在 request path reflection 查 type/method name。
更新规则:
Runtime compile 必须复用当前 Build-time validation/ID resolution 原则。若基于 contract type/method name 的 rule 需要 resolve,应 against 当前可用 generated manifest/service snapshot;不要引入 reflection fallback。
Phase 7 — Rate limiter 更新
支持:
这是比 concurrency 更高风险的部分。
硬性不变量
配置更新不得凭空产生免费 burst。
例如 old token bucket 已耗尽,update 后不能因为 new BCL limiter 初始 full capacity 而立刻多放一整桶请求。
可接受实现路线 A:state-preserving dynamic implementation
实现本项目自己的 narrow admission rate state,保存:
同算法参数变化时 reconcile 当前状态。
优点:语义最好。风险:实现 fixed/sliding window correctness 较复杂,必须大量 fake-time 测试。
可接受实现路线 B:conservative generation transition
继续使用 BCL RateLimiter,但配置变化视为 structural generation transition。
要求:
禁止:直接
new TokenBucketRateLimiter(newOptions)后立即 publish fresh full bucket,测试只检查“最终能请求”而忽略 burst。Algorithm change
例如:
视为 structural change:
Phase 8 — Partition 动态化
区分两类:
同 selector 下参数更新
MaxPartitions;IdleTimeout;可以在保持 partition namespace 的前提下 reconcile/update。
Shrink
MaxPartitions时不要驱逐 active partition;按 idle reclaim 逐步回落。selector replacement
例如:
这是新的 namespace,必须创建新
AdmissionPartitionGeneration。不要迁移 old dictionary entry 到新 selector。
Phase 9 — Enable / Disable / Re-enable
Disable:
优先用 null 表示 disabled,不使用 NoopAdmissionController。
新 request:直接 bypass。
old request:继续 old generation。
不要调用 shutdown
StopAccepting()取消旧 queue。Re-enable:
Phase 10 — Server Stop / Dispose 整合
Server shutdown 与 runtime disable 是不同语义。
Shutdown:
必须避免:
Update 与 Stop 需要同一个 lifecycle barrier/control gate 语义。
Runtime control API
最终 public naming 可在 PR review 决定,但底层至少需要表达:
候选形式:
或独立 control handle。
无论 API 形式如何:
SharpLinkAdmissionControlOptions作为 candidate builder 使用;UseAdmissionControl仍负责 initial generation。不要在本 issue 顺便统一所有 Runtime Configuration API。
热路径设计要求
Disabled path
目标:
要求:
Enabled immediate-permit path
动态支持不得让每个 limiter slot 都重新读取 global config。
优先:
现有 unit test:
ImmediateAdmissionShouldNotAllocateThreeTransientArraysPerCall必须保留且建议进一步收紧/优化;动态化不得明显增加 immediate admission allocation。
Precompiled route 优化(建议)
当前 scope 最大通常是:
每层最多 concurrency + rate。
Program build/update path 可以预计算 static Global/Contract/Method execution plan,避免每个 request 重新拼 immutable route/临时集合。
Partition 因 key 动态解析,可最后追加。
这个优化不是为了“代码漂亮”,而是为了抵消 enabled generation capture/lifecycle 成本。
测试实施手册
主文件:
test/SharpLink.UnitTests/Server/AdmissionControlTests.cs建议新增:
test/SharpLink.UnitTests/Server/DynamicAdmissionControlTests.cstest/SharpLink.IntegrationTests/DynamicAdmissionIntegrationTests.cs避免让已有单文件无限膨胀,但复用现有 helper/fake rate limiter。
A. Enable / Disable
B. Concurrency resize — 必须 deterministic
Increase:
Shrink:
必须覆盖并发 acquire/release/update stress,并断言 max observed active 不因 generation replacement超过语义上允许值。
C. Disable / Re-enable 与 old active overlap
关键回归:
如果 old state 尚未 drain,不能让 new generation 再接 2 个导致 admission accounting 被拆成 4。
测试必须能发现这种 naive-controller-swap bug。
D. Queue policy update
E. Rate update
三个算法分别测试:
所有时间相关测试优先使用项目统一
TimeProvider/ fake time,不要依赖Task.Delay+ wall clock 猜窗口。F. Rule routing
G. Partition
H. RequestLoop generation consistency
这是与普通 limiter 单测不同的重点集成测试。
必须覆盖:
如果无法直接 deterministic 卡在 frame capture 点,可使用测试 transport/barrier 构造;不要只用高概率 stress 代替核心 deterministic test。
I. Compression / pre-admission payload
Admission enable/disable 会改变 Request payload decode 时机,因此至少测试:
J. Cancellation / Deadline / Stop
K. Stress
运行:
持续检查:
Benchmark / 性能门禁
主文件:
test/SharpLink.Benchmarks/AdmissionBenchmarks.cs必须扩展现有 benchmark,而不是只跑 micro-controller benchmark。
End-to-end RPC scenarios
至少:
Disabled— dynamic-capable server but current program null;ImmediatePermit— global concurrency ample;ImmediatePermitAfterResize— 多次 update 后 steady state;ImmediateReject;MethodRuleImmediatePermit;PartitionImmediatePermit。Controller/state microbench
至少:
需要报告
性能要求
Disabled:
AdmissionRpcBenchmarks.Disabled的稳定回退应在噪声范围;持续 > 约 3%–5% 必须分析。Enabled immediate permit:
Update 本身不是 request-rate 操作,可以分配/加 control lock,但应有 stress 证明 publication pause 不扩散到 request threads。
验证命令
基础:
按影响范围补:
BenchmarkDotNet:至少运行
AdmissionRpcBenchmarks、AdmissionControllerBenchmarks及新增 dynamic admission benchmark,并在 PR 附 before/after 数据。风险清单
R1 — Request decode generation 混用
最高 correctness 风险。RequestLoop 与 AdmissionDispatch 必须共享同一个 capture。
R2 — Concurrency 更新反而超限
任何 fresh controller active=0 的方案都会有这个风险。
R3 — Rate update 免费 burst
不能因为配置变化重置 token/window quota。
R4 — Retired state use-after-dispose
Atomic swap 不等于安全 reclaim。必须 generation use/drain 或等价机制。
R5 — 历史 generation 永久保留
为了避免 use-after-dispose 而永不回收也不接受;更新次数不能线性永久增长内存。
R6 — Queue generation 分裂
多个 controller 各自 queue accounting 会破坏 global bound。
R7 — Disable 错用 shutdown semantics
普通 Disable 不应该调用当前 shutdown-style
StopAccepting()把 queued calls 变成 Unavailable。R8 — Partition selector 状态错误迁移
不同 selector namespace 不能复用 old partition dictionary。
R9 — Stop 与 update 竞态
Stop seal 后绝不能 publish 新 generation。
R10 — OneWay 行为回归
QueueOneWayCalls、drop、rejected metrics、stream draining 都要单独验证。
R11 — Pre-admission stream/buffer owner leak
queued request 会 retain payload/stream resources,generation update 不得导致 decoded owner、copied payload、stream reservation 丢失或 double release。
R12 — 把 Connection Admission 混进来
SharpLinkConnectionAdmissionOptions/ connection-level admission 是独立生命周期与入口。本 issue 默认只处理 RPC Server Admission Control;connection admission 动态化另开 issue,避免扩大状态空间。明确禁止的实现
SharpLinkAdmissionControlOptions直接变成线程安全 mutable runtime object。lock(config)。ConcurrentDictionary全面替换 Frozen route 只为了支持 update。new SharpLinkAdmissionController简单 replacement 处理 concurrency resize。Out of scope
MaxConcurrentCallsPerServer/ flow-control 全部统一进本 Admission 模块;可复用设计但另议。建议提交拆分
本 issue 可以由一个聚焦 PR 完成,也可在同一 issue 下拆成顺序 PR;若拆 PR,每一步必须保持可运行,不允许长期保留两套互相竞争的 admission path。
建议 commits / PR stages:
test: lock admission generation and accounting invariantsrefactor: capture admission generation at request frame boundaryrefactor: split admission program from runtime statefeat: add runtime enable disable and concurrency resizefeat: add dynamic queue and rule updatesfeat: add safe rate and partition transitionstest: add dynamic admission integration and stress coverageperf: extend admission benchmarksdocs: document runtime admission semantics如果 rate/partition 复杂度需要独立 PR,可在本 issue 下继续,不要把未完成部分静默标 Done。
Definition of Done
UseAdmissionControl仍作为 initial configuration 正常工作。AdmissionControlTests当前行为全部继续通过。AdmissionRpcBenchmarks/AdmissionControllerBenchmarks已扩展并提供 before/after 数据。doc/admission-control.md明确 runtime update 生效点、旧 request/queue/lease 处理与 rate/partition transition semantics。Checks
dev为分析与实施基线。