You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue is the implementation manual for this slice. Re-check #328 before branching and use its latest reviewed exact head if it changes.
Goal
Expose the first production runtime control operation for Server Admission: atomic enable and disable.
#323 established per-Request generation capture. #328 established the stable AdmissionStateKernel, compatible state reuse, safe retire/reclaim, and Stop sealing. This slice should now promote those internal/test-only publication mechanics into a production control path without adding general policy update/resize semantics yet.
Required behavior:
Disabled
-> build + validate candidate off hot path
-> atomically publish AdmissionProgram N
-> new Requests capture N
Enabled N
-> atomically publish Disabled
-> retire N
-> new Requests bypass policy Admission
-> old queued/active Requests keep N until normal terminal release
-> N/state reclaim when safe
This slice must support initial-disabled -> enable, initial-enabled -> disable, and disable -> re-enable. It must not implement arbitrary enabled N -> changed N+1 configuration updates.
Stable lifecycle owner already exists even when initially disabled
SharpLinkServerBuilder.Materialize(...) always creates a SharpLinkAdmissionController lifecycle owner:
configured server -> SharpLinkAdmissionController.Create(...);
disabled server -> SharpLinkAdmissionController.CreateDisabled(...).
The disabled controller owns the stable AdmissionStateKernel, so an initially disabled server can create its first runtime AdmissionProgram without replacing the kernel.
cleans partially built/unpublished bindings on failure.
Publication primitive is currently test-only
PublishAdmissionProgramForTests(...) already demonstrates the required atomic shape:
candidate must belong to the same kernel;
retired candidates cannot be republished;
publication is serialized with server lifecycle state;
Stop/Drain rejects publication and retires the candidate;
previous enabled publication is retired after replacement.
Do not create a second production implementation with subtly different semantics. Refactor/promote one shared production publication primitive and let tests call the same path.
Public API package boundary matters
ISharpLinkServer lives in SharpLink.Abstractions, while SharpLinkAdmissionControlOptions lives in SharpLink.Server.
Do not create an Abstractions -> Server project dependency merely to add an Admission method to ISharpLinkServer.
Recommended minimal surface is a public API in the Server package, for example extension methods over ISharpLinkServer backed by an internal runtime-control capability implemented by SharpLinkServer:
Exact naming may change in review. A dedicated Server-package control handle is also acceptable. The invariants below matter more than the spelling.
Unsupported ISharpLinkServer implementations should fail predictably with NotSupportedException; null arguments should follow normal argument validation.
Semantics
Enable
Enable is valid only when the current publication is disabled.
Control path:
allocate a fresh SharpLinkAdmissionControlOptions candidate;
invoke the caller configure delegate outside server publication locks;
fully validate/deep-compile the candidate;
create a candidate AdmissionProgram in the existing server kernel;
enter the short publication/lifecycle critical section;
verify the server is not draining/stopped/faulted;
verify the current publication is still disabled;
atomically publish the candidate;
return only after publication is visible.
If another writer enabled Admission first, this call must not silently become an update. The losing enable must fail predictably and safely retire/reclaim its unpublished candidate.
Recommended behavior: throw InvalidOperationException when Admission is already enabled. General enabled -> enabled update belongs to later slices.
Disable
Disable atomically publishes the disabled sentinel and retires the previously current program.
Disable must not:
call shutdown-style StopAccepting();
cancel already queued Admission waits;
cancel active calls/leases;
wait synchronously for the retired generation to reclaim;
change ServerResourceGovernor behavior.
After Disable returns, Requests whose capture starts afterward must bypass policy Admission. Requests that captured the old generation before Disable continue with that generation.
Repeated Disable while already disabled should be an idempotent no-op unless a stronger API reason is found during review.
Re-enable
Re-enable after Disable builds a new immutable program from caller-supplied options.
If compatible old state is still live because the retired generation has queued/active users, #328 state identity/reuse must prevent split accounting. Example:
N limit=1
A owns permit under N
Disable
Enable same compatible policy as N+1
B under N+1 must still observe A's permit usage
If the previous generation and its state have already fully reclaimed, re-enable may create fresh state. This slice does not promise preservation of rate/window history across an arbitrary interval where Admission was explicitly disabled and all old state was reclaimed.
It does promise no duplicate bound/state while compatible retired state is still live.
The public surface should stay deliberately small in this slice.
It must express:
enable from a complete candidate configuration;
disable current Admission.
It must not expose partial mutations such as SetMaxQueuedCalls, ResizeConcurrency, ReplaceRateLimit, etc.
Requirements:
caller options are a candidate builder only; runtime request paths never read the mutable options object;
caller retaining/mutating the options instance after Enable returns cannot change published behavior;
validation failure is transactional: current publication/state does not change;
configure delegate exceptions are transactional: current publication/state does not change;
control writers are serialized/linearizable;
control locks are never taken by normal Request admission/capture;
no public API should expose internal AdmissionProgram, kernel, controller, or generation-use objects.
Do not move SharpLinkAdmissionControlOptions into Abstractions solely for this feature.
Manifest / rule-resolution boundary
For this slice, preserve the existing Admission compile semantics unless a correctness bug requires more:
runtime candidate compilation may use the server's existing static generated-manifest snapshot, as current CreateAdmissionProgramForTests(...) does;
stable contract/method ID overloads remain valid without type/name reflection;
do not broaden this PR into dynamic-module Admission rule reconciliation.
If type/name rules for assemblies registered dynamically after server build require a broader live-manifest snapshot, document that dependency for the later rule add/remove/update slice instead of silently redesigning dynamic module ownership here.
Implementation sequence
1. Refactor one production publication primitive
Extract the publication logic currently exercised by PublishAdmissionProgramForTests(...) into one internal production method used by:
public Enable;
public Disable;
test hooks/helpers;
Stop sealing where applicable.
There must be one source of truth for:
same-kernel validation;
server lifecycle state check;
current publication check;
atomic write;
previous-program retirement;
losing/unpublished candidate cleanup.
Do not invoke user callbacks while holding _registryGate or kernel registry/accounting locks.
2. Add public Enable surface
Create fresh options per call, configure/validate on control path, then create the immutable candidate program.
Before publication, force deterministic test seams around:
candidate built but not yet published;
server Stop begins;
another Enable/Disable writer wins publication.
Every failed/lost candidate must be retired/reclaimed without state-registry leaks.
3. Add public Disable surface
Atomically publish disabled and retire the previous generation.
Return after the new disabled publication is visible, not after old queued/active work drains.
4. Preserve state overlap on re-enable
Use #328 kernel identity/reuse exactly as designed. Do not create a second kernel/controller for re-enable.
Deterministically prove concurrency, queue, rate, and partition state are not duplicated while old compatible state is still referenced.
No parameter changes are required in this slice: re-enable tests should use identical compatible definitions.
5. Integrate Stop / lifecycle races
A candidate can be expensive to build, so Stop may race after candidate creation and before publication.
Required behavior:
build candidate
Stop seals server
publish attempt loses
candidate retires/reclaims
no new program becomes current
No deadlock and no state leak.
Once server state is draining/stopped/faulted, Enable must fail. Disable after lifecycle sealing must not resurrect or disturb shutdown semantics; exact exception/no-op behavior should be documented and deterministic.
6. Documentation
Update doc/admission-control.md (or the current Admission documentation location) with:
runtime Enable/Disable API;
exact effective boundary: next Request capture;
old queued/active Request behavior;
ordinary Disable vs Server Stop distinction;
re-enable state-overlap semantics;
current limitation that enabled -> changed enabled policy update is not yet supported.
Required test matrix
Public Enable
initially disabled server can Enable global concurrency policy;
newly captured Requests are governed immediately after Enable returns;
initially disabled Request captured before Enable remains disabled/bypass;
configured OneWay/queue/rate/partition behavior works after runtime Enable;
retained caller options mutation after return cannot alter published program;
unsupported ISharpLinkServer implementation returns NotSupportedException through the chosen Server-package control surface.
Public Disable
initially enabled server can Disable;
new Requests after Disable return bypass policy Admission;
active old-generation Request continues normally;
queued old-generation one-way continues according to captured old policy;
queued old-generation two-way continues according to captured old policy;
repeated Disable is safe/idempotent;
retired generation eventually reclaims when old users release.
Disable -> re-enable overlap
old concurrency permit constrains compatible re-enabled generation;
old queued calls and re-enabled generation share one queue accounting kernel;
compatible consumed rate state is reused while old state is still live;
compatible partition namespace/state is reused while old state is still live;
no duplicate rule/partition registry entry for compatible overlapping state;
after all old users drain, retired program count returns to zero.
Transactionality / invalid candidates
configure delegate throws -> current publication unchanged;
options validation fails -> current publication unchanged;
contract/method resolution fails -> current publication unchanged;
partial candidate construction failure releases acquired bindings;
Enable while already enabled does not become an accidental update;
losing concurrent Enable candidate is fully reclaimed.
Writer races
Use deterministic barriers, not timing luck:
two concurrent Enable calls from disabled: exactly one publication wins; loser fails/reclaims;
Enable vs Disable is linearizable and final state matches publication order;
repeated enable/disable cycles do not grow live/retired program or rule/partition state unboundedly;
no queue/permit counter underflow during writer races.
Stop races
candidate built -> Stop seals -> Enable publish rejected and candidate reclaimed;
Disable racing Stop cannot deadlock;
no program publication after Stop seal;
Stop with retired queued/active generation still drains/disposes exactly once;
final kernel diagnostics return to zero.
ResourceGovernor / compression regression
runtime-disabled Admission does not bypass call capacity;
After runtime Disable, Request path must remain equivalent to the #328 disabled path:
no request-path lock;
no generation refcount;
no no-op controller/kernel call;
no per-Request allocation;
non-Request frames unchanged.
A server that was enabled and then disabled must not carry extra per-Request work merely because it was previously enabled.
Enabled steady state
Runtime-enabled steady state must use the same precompiled AdmissionProgram/kernel path as build-time enabled Admission. Do not introduce an extra public-control indirection on each Request.
Control path
Enable may allocate, validate, resolve IDs, build immutable bindings, and take control locks. Disable may take the short publication lock. Neither operation should block normal Request threads on a configuration lock.
Add/extend benchmarks if needed to compare:
initially disabled steady state;
enabled-at-build steady state;
runtime-enabled steady state;
enabled -> runtime-disabled steady state.
If runtime-enabled/disabled steady state differs materially from the equivalent build-time state, investigate before Ready for Review.
unified runtime-configuration API across unrelated subsystems;
connection-level Admission dynamic configuration;
ResourceGovernor/decode/fair-scheduler changes.
If a general update primitive is needed merely to implement Enable/Disable, keep it internal and prove this public slice cannot invoke enabled -> changed-enabled transitions.
Run targeted deterministic public-control/lifecycle tests during development.
Before leaving Draft, run full exact-head PR Quick.
PR body must record exact head SHA, PR Quick run ID, Unit/Generator/Load/Integration counts, AOT/package/load/chaos results, and enable/disable lifecycle diagnostics.
Any valid review finding gets deterministic regression coverage plus a new exact-head CI run.
Do not merge without explicit user authorization.
Definition of Done
production public Server-package API can Enable Admission on an initially disabled running server;
production public API can Disable current Admission atomically;
public API respects Abstractions/Server dependency direction;
Enable uses complete candidate build/validation before publication;
failed/losing candidates leave current publication unchanged and reclaim exactly once;
Enable while already enabled cannot silently become policy update;
Disable is next-Request semantics and does not cancel queued/active old-generation work;
disable -> re-enable overlap reuses compatible kernel state and cannot split concurrency/queue/rate/partition bounds;
Parent: #264
Architecture dependency: #273
Previous slice: #324 / PR #328
Stack base: reviewed #328 exact head
21a3ebef8e05b97dffd49cc13c5cf1209153d657.Goal
Expose the first production runtime control operation for Server Admission: atomic enable and disable.
#323 established per-Request generation capture. #328 established the stable
AdmissionStateKernel, compatible state reuse, safe retire/reclaim, and Stop sealing. This slice should now promote those internal/test-only publication mechanics into a production control path without adding general policy update/resize semantics yet.Required behavior:
This slice must support initial-disabled -> enable, initial-enabled -> disable, and disable -> re-enable. It must not implement arbitrary enabled N -> changed N+1 configuration updates.
Current code facts on #328
Stable lifecycle owner already exists even when initially disabled
SharpLinkServerBuilder.Materialize(...)always creates aSharpLinkAdmissionControllerlifecycle owner:SharpLinkAdmissionController.Create(...);SharpLinkAdmissionController.CreateDisabled(...).The disabled controller owns the stable
AdmissionStateKernel, so an initially disabled server can create its first runtimeAdmissionProgramwithout replacing the kernel.Production request capture / lifecycle is ready
SharpLinkServer.AdmissionProgram.csalready has:_admissionProgramimmutable publication pointer;AdmissionProgram.Disabled/Uninitializedsentinels;CaptureAdmissionProgram(...);TryAcquireUse();Candidate build primitive already exists
AdmissionStateKernel.CreateProgram(options, manifests):Publication primitive is currently test-only
PublishAdmissionProgramForTests(...)already demonstrates the required atomic shape:Do not create a second production implementation with subtly different semantics. Refactor/promote one shared production publication primitive and let tests call the same path.
Public API package boundary matters
ISharpLinkServerlives inSharpLink.Abstractions, whileSharpLinkAdmissionControlOptionslives inSharpLink.Server.Do not create an
Abstractions -> Serverproject dependency merely to add an Admission method toISharpLinkServer.Recommended minimal surface is a public API in the Server package, for example extension methods over
ISharpLinkServerbacked by an internal runtime-control capability implemented bySharpLinkServer:Exact naming may change in review. A dedicated Server-package control handle is also acceptable. The invariants below matter more than the spelling.
Unsupported
ISharpLinkServerimplementations should fail predictably withNotSupportedException; null arguments should follow normal argument validation.Semantics
Enable
Enable is valid only when the current publication is disabled.
Control path:
SharpLinkAdmissionControlOptionscandidate;AdmissionProgramin the existing server kernel;If another writer enabled Admission first, this call must not silently become an update. The losing enable must fail predictably and safely retire/reclaim its unpublished candidate.
Recommended behavior: throw
InvalidOperationExceptionwhen Admission is already enabled. General enabled -> enabled update belongs to later slices.Disable
Disable atomically publishes the disabled sentinel and retires the previously current program.
Disable must not:
StopAccepting();ServerResourceGovernorbehavior.After Disable returns, Requests whose capture starts afterward must bypass policy Admission. Requests that captured the old generation before Disable continue with that generation.
Repeated Disable while already disabled should be an idempotent no-op unless a stronger API reason is found during review.
Re-enable
Re-enable after Disable builds a new immutable program from caller-supplied options.
If compatible old state is still live because the retired generation has queued/active users, #328 state identity/reuse must prevent split accounting. Example:
If the previous generation and its state have already fully reclaimed, re-enable may create fresh state. This slice does not promise preservation of rate/window history across an arbitrary interval where Admission was explicitly disabled and all old state was reclaimed.
It does promise no duplicate bound/state while compatible retired state is still live.
Existing Request consistency
Keep #323 invariants unchanged:
Resource safety
Dynamic policy disable only disables policy Admission.
It must never bypass:
ServerResourceGovernor;RequestPermit;#273/#319 resource invariants remain authoritative.
Public control API requirements
The public surface should stay deliberately small in this slice.
It must express:
It must not expose partial mutations such as
SetMaxQueuedCalls,ResizeConcurrency,ReplaceRateLimit, etc.Requirements:
AdmissionProgram, kernel, controller, or generation-use objects.Do not move
SharpLinkAdmissionControlOptionsinto Abstractions solely for this feature.Manifest / rule-resolution boundary
For this slice, preserve the existing Admission compile semantics unless a correctness bug requires more:
CreateAdmissionProgramForTests(...)does;If type/name rules for assemblies registered dynamically after server build require a broader live-manifest snapshot, document that dependency for the later rule add/remove/update slice instead of silently redesigning dynamic module ownership here.
Implementation sequence
1. Refactor one production publication primitive
Extract the publication logic currently exercised by
PublishAdmissionProgramForTests(...)into one internal production method used by:There must be one source of truth for:
Do not invoke user callbacks while holding
_registryGateor kernel registry/accounting locks.2. Add public Enable surface
Create fresh options per call, configure/validate on control path, then create the immutable candidate program.
Before publication, force deterministic test seams around:
Every failed/lost candidate must be retired/reclaimed without state-registry leaks.
3. Add public Disable surface
Atomically publish disabled and retire the previous generation.
Return after the new disabled publication is visible, not after old queued/active work drains.
4. Preserve state overlap on re-enable
Use #328 kernel identity/reuse exactly as designed. Do not create a second kernel/controller for re-enable.
Deterministically prove concurrency, queue, rate, and partition state are not duplicated while old compatible state is still referenced.
No parameter changes are required in this slice: re-enable tests should use identical compatible definitions.
5. Integrate Stop / lifecycle races
A candidate can be expensive to build, so Stop may race after candidate creation and before publication.
Required behavior:
No deadlock and no state leak.
Once server state is draining/stopped/faulted, Enable must fail. Disable after lifecycle sealing must not resurrect or disturb shutdown semantics; exact exception/no-op behavior should be documented and deterministic.
6. Documentation
Update
doc/admission-control.md(or the current Admission documentation location) with:Required test matrix
Public Enable
ISharpLinkServerimplementation returnsNotSupportedExceptionthrough the chosen Server-package control surface.Public Disable
Disable -> re-enable overlap
Transactionality / invalid candidates
Writer races
Use deterministic barriers, not timing luck:
Stop races
ResourceGovernor / compression regression
Existing suite
AdmissionStateKernellifecycle/reclamation tests remain green;Performance requirements
Disabled steady state
After runtime Disable, Request path must remain equivalent to the #328 disabled path:
A server that was enabled and then disabled must not carry extra per-Request work merely because it was previously enabled.
Enabled steady state
Runtime-enabled steady state must use the same precompiled
AdmissionProgram/kernel path as build-time enabled Admission. Do not introduce an extra public-control indirection on each Request.Control path
Enable may allocate, validate, resolve IDs, build immutable bindings, and take control locks. Disable may take the short publication lock. Neither operation should block normal Request threads on a configuration lock.
Add/extend benchmarks if needed to compare:
If runtime-enabled/disabled steady state differs materially from the equivalent build-time state, investigate before Ready for Review.
Out of scope
Do not implement here:
MaxQueuedCalls/MaxQueuedBytes/MaxQueueDelaydynamic changes;QueueOneWayCallschange while continuously enabled;MaxPartitions/IdleTimeoutchange;IOptionsMonitorintegration;If a general update primitive is needed merely to implement Enable/Disable, keep it internal and prove this public slice cannot invoke enabled -> changed-enabled transitions.
PR / validation procedure
21a3ebef8e05b97dffd49cc13c5cf1209153d657at issue creation).issue-324-admission-state-kernel/ PR refactor(server): extract admission state kernel lifecycle #328.Definition of Done
ServerResourceGovernorremains always enforced independent of policy Admission state;Follow-up slices
After this slice is reviewed, #264 continues with:
Refs #264, #273, #324, #328, #322, #323, #319.