Skip to content

[Runtime Configuration] Dynamic Server Admission Control:高性能启停、配置更新与状态安全迁移 #264

Description

@SunSi12138

总跟踪:#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() 会:

  1. StopAccepting()
  2. 等 queue drain;
  3. 等 active permits drain;
  4. 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

更新为:

new limit = 50

如果:

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 方案也可以,但必须证明:

  1. 无 use-after-dispose;
  2. retired generations 最终可回收;
  3. 不允许“为了简单”把所有历史 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 时:

  1. 原子读取当前 Admission Program;
  2. 若 non-null,获得安全 generation use;
  3. 使用这个 captured generation 决定 ValidateEnvelope vs DecodeInboundPayload
  4. 将 generation/use 传入 DispatchOneWayRpc / DispatchRpcAsync
  5. 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:

limit 50 -> 100

应立即允许更多新 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:

current -> null

优先用 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 形式如何:

  1. SharpLinkAdmissionControlOptions 作为 candidate builder 使用;
  2. 不允许 runtime request path 长期读取 mutable options object;
  3. update 完整 clone/build/validate;
  4. validation failure 不改变 current program/state;
  5. update writer 串行化,避免 read-modify-publish lost update;
  6. lifecycle stopped/disposed 后禁止再次 publish;
  7. Builder UseAdmissionControl 仍负责 initial generation。

不要在本 issue 顺便统一所有 Runtime Configuration API。


热路径设计要求

Disabled path

目标:

Request frame
    ↓
1 atomic/volatile program pointer read
    ↓
null branch
    ↓
现有 direct decode + dispatch

要求:

  • 无 per-request allocation;
  • 无 lock;
  • 无 no-op controller call;
  • 无 generation refcount(null 时);
  • 不改变非 Request frame fast path。

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

  • server 初始 disabled,enable global concurrency 后新请求受限。
  • server 初始 enabled,disable 后新请求 bypass。
  • disable 不取消已有 active lease。
  • disable 不取消已有 queued request(普通 runtime disable)。
  • queued old request 完成后 accounting 回到 0。
  • repeated enable/disable 不 leak generation/state。

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

  • MaxQueuedCalls increase 即时允许新 waiter。
  • shrink 不杀已有 waiter,新 waiter 在 queued > new limit 时拒绝。
  • MaxQueuedBytes 同样测试。
  • MaxQueueDelay 在 enqueue 时 capture;update 不改变已有 waiter budget。
  • QueueOneWayCalls 只影响新 OneWay。
  • queue accounting 在多 generation overlap 时仍是全局一致的。

E. Rate update

三个算法分别测试:

  • TokenBucket 参数更新不产生免费 full-bucket burst;
  • FixedWindow 更新不凭空重置完整窗口 quota;
  • SlidingWindow 更新不凭空重置全部 segments;
  • old queued retained rate lease 在 update 后不会 double-consume;
  • algorithm replacement 不 use-after-dispose;
  • update 后 eventual new policy 正常生效。

所有时间相关测试优先使用项目统一 TimeProvider / fake time,不要依赖 Task.Delay + wall clock 猜窗口。

F. Rule routing

  • add Contract rule;
  • remove Contract rule;
  • add Method rule;
  • replace Method rule;
  • Global + Contract + Method composite 顺序/约束不变;
  • invalid/unknown rule resolution 不 publish candidate;
  • invalid update 后 old program 继续正常工作。

G. Partition

  • MaxPartitions increase;
  • MaxPartitions shrink 不驱逐 active entry;
  • IdleTimeout update;
  • same selector 下 compatible state preservation;
  • selector replacement creates new generation;
  • old active partition 完成后 old generation 可 reclaim;
  • default partition 与业务 key 仍不 alias;
  • repeated selector updates 不造成无界 retired pool。

H. RequestLoop generation consistency

这是与普通 limiter 单测不同的重点集成测试。

必须覆盖:

  1. Request frame capture old enabled generation;
  2. frame 已选择 pre-admission(未 decode);
  3. 配置线程 disable/replace;
  4. old Request 仍按 old generation acquire + deferred decode;
  5. 新 Request 使用新 generation / disabled path。

如果无法直接 deterministic 卡在 frame capture 点,可使用测试 transport/barrier 构造;不要只用高概率 stress 代替核心 deterministic test。

I. Compression / pre-admission payload

Admission enable/disable 会改变 Request payload decode 时机,因此至少测试:

  • compressed unary request;
  • compressed OneWay request;
  • client-streaming / duplex 的 pre-admission compressed stream data;
  • update while request is queued;
  • update 后无 double decode / skipped decode / buffer owner leak;
  • rejected request 正确 drain pre-admission streams。

J. Cancellation / Deadline / Stop

  • queued request cancellation during update;
  • deadline expiry during update;
  • disable 与 cancellation race;
  • update 与 Server Stop race;
  • Stop 后不能 publish new generation;
  • server Dispose waits admission resources exactly once;
  • no counter underflow / no queued bytes leak。

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

至少:

  1. Disabled — dynamic-capable server but current program null;
  2. ImmediatePermit — global concurrency ample;
  3. ImmediatePermitAfterResize — 多次 update 后 steady state;
  4. ImmediateReject
  5. MethodRuleImmediatePermit
  6. 如 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:

  • 无新增 allocation;
  • 无 request-path lock;
  • 理想额外成本仅一次 program pointer load + branch;
  • 相对现有 AdmissionRpcBenchmarks.Disabled 的稳定回退应在噪声范围;持续 > 约 3%–5% 必须分析。

Enabled immediate permit:

  • 不因 dynamic update 每次重新 build route;
  • generation lifetime accounting 不出现明显全局锁竞争;
  • allocation 不应明显高于现有 immediate path;
  • control-plane update 不阻塞 RPC worker。

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:至少运行 AdmissionRpcBenchmarksAdmissionControllerBenchmarks 及新增 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:

  1. test: lock admission generation and accounting invariants
  2. refactor: capture admission generation at request frame boundary
  3. refactor: split admission program from runtime state
  4. feat: add runtime enable disable and concurrency resize
  5. feat: add dynamic queue and rule updates
  6. feat: add safe rate and partition transitions
  7. test: add dynamic admission integration and stress coverage
  8. perf: extend admission benchmarks
  9. docs: document runtime admission semantics

如果 rate/partition 复杂度需要独立 PR,可在本 issue 下继续,不要把未完成部分静默标 Done。


Definition of Done

  • Builder UseAdmissionControl 仍作为 initial configuration 正常工作。
  • Server runtime 可 enable / disable / replace Admission configuration。
  • Request frame 在决定 pre-admission decode 前 capture admission generation。
  • 同一 Request 的 RequestLoop、Admission acquire、async queue、deferred decode、lease release 使用同一 generation。
  • disabled path 使用 null/empty program fast path,无 no-op controller。
  • update 使用完整 candidate validation + state reconciliation + atomic publication。
  • policy/program 与 mutable runtime state 已分离。
  • concurrency resize 保留 active count,shrink 不杀旧调用且不允许新 controller 叠加超限。
  • enable/disable/re-enable 在 old active/queued overlap 时 accounting 正确。
  • queue count/bytes shrink 不取消已有 waiter,new waiter 使用新 policy。
  • MaxQueueDelay 有明确 capture semantics。
  • rate update 不产生免费 burst,三个算法都有 deterministic test。
  • rate algorithm replacement 有 retire/drain semantics。
  • rule add/remove/replace 使用 immutable route publication。
  • partition selector replacement 使用独立 generation,不错误迁移 namespace state。
  • retired generation 无 use-after-dispose,并最终可 reclaim。
  • Stop/Dispose seal update path,并等待所有 admission-owned state exactly once。
  • compressed Request / OneWay / streaming pre-admission 路径有动态切换集成测试。
  • cancellation/deadline/update/stop 竞态有测试。
  • AdmissionControlTests 当前行为全部继续通过。
  • AdmissionRpcBenchmarks / AdmissionControllerBenchmarks 已扩展并提供 before/after 数据。
  • disabled path 无新增 per-request allocation、无 lock,性能回退处于噪声范围或有明确证据说明。
  • doc/admission-control.md 明确 runtime update 生效点、旧 request/queue/lease 处理与 rate/partition transition semantics。

Checks

  • dev 为分析与实施基线。
  • 已搜索现有 open issues,仅存在总跟踪 [Runtime Configuration] 动态配置 / Hot Reload 长期跟踪 #262,没有重复 Dynamic Server Admission Control 实施 issue。
  • 本 issue 明确覆盖现状原因、RequestLoop/Dispatch/Controller 路径、状态模型、实施阶段、测试、benchmark、风险、禁止项与 DoD。

Activity

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