Skip to content

[Server][DynamicAdmission] Add atomic runtime enable/disable control #329

Description

@SunSi12138

Parent: #264
Architecture dependency: #273
Previous slice: #324 / PR #328
Stack base: reviewed #328 exact head 21a3ebef8e05b97dffd49cc13c5cf1209153d657.

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.

Current code facts on #328

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.

Production request capture / lifecycle is ready

SharpLinkServer.AdmissionProgram.cs already has:

  • _admissionProgram immutable publication pointer;
  • AdmissionProgram.Disabled / Uninitialized sentinels;
  • lock-free CaptureAdmissionProgram(...);
  • stale-read retry through TryAcquireUse();
  • retirement/reclamation safety from refactor(server): extract admission state kernel lifecycle #328;
  • Stop publication sealing under the server lifecycle/registry gate.

Candidate build primitive already exists

AdmissionStateKernel.CreateProgram(options, manifests):

  • rejects creation after shutdown sealing;
  • resolves/builds immutable program bindings;
  • reuses compatible rule/rate/partition state;
  • 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:

server.EnableAdmissionControl(options => { ... });
server.DisableAdmissionControl();

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:

  1. allocate a fresh SharpLinkAdmissionControlOptions candidate;
  2. invoke the caller configure delegate outside server publication locks;
  3. fully validate/deep-compile the candidate;
  4. create a candidate AdmissionProgram in the existing server kernel;
  5. enter the short publication/lifecycle critical section;
  6. verify the server is not draining/stopped/faulted;
  7. verify the current publication is still disabled;
  8. atomically publish the candidate;
  9. 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.

Existing Request consistency

Keep #323 invariants unchanged:

  • Request captures at most one generation;
  • queued continuation never re-reads current publication;
  • Disable does not move an old Request onto disabled semantics;
  • Enable does not pull a previously disabled-capture Request into the new generation.

Resource safety

Dynamic policy disable only disables policy Admission.

It must never bypass:

  • ServerResourceGovernor;
  • call reservation / RequestPermit;
  • decode permits / retained-byte / decoded-byte budgets;
  • pre-admission stream byte budgets;
  • physical stream ownership.

#273/#319 resource invariants remain authoritative.

Public control API requirements

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

Existing suite

Performance requirements

Disabled steady state

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.

Out of scope

Do not implement here:

  • enabled N -> changed N+1 policy update;
  • concurrency numeric resize;
  • MaxQueuedCalls / MaxQueuedBytes / MaxQueueDelay dynamic changes;
  • dynamic QueueOneWayCalls change while continuously enabled;
  • TokenBucket/FixedWindow/SlidingWindow parameter migration;
  • rate algorithm replacement;
  • Contract/Method runtime rule add/remove/replace;
  • partition selector replacement or dynamic MaxPartitions / IdleTimeout change;
  • dynamic-module Admission policy reconciliation redesign;
  • Hosting / IOptionsMonitor integration;
  • 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.

PR / validation procedure

  • Create the implementation branch from the latest reviewed refactor(server): extract admission state kernel lifecycle #328 head (21a3ebef8e05b97dffd49cc13c5cf1209153d657 at issue creation).
  • Stack the Draft PR on issue-324-admission-state-kernel / PR refactor(server): extract admission state kernel lifecycle #328.
  • Keep implementation limited to this issue.
  • 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;
  • capture/generation invariants from feat(server): capture admission program generation per request #323 remain intact;
  • retire/reclaim/Stop invariants from refactor(server): extract admission state kernel lifecycle #328 remain intact;
  • runtime-disabled path remains lock/refcount/allocation free;
  • ServerResourceGovernor remains always enforced independent of policy Admission state;
  • docs describe runtime enable/disable semantics and current limitations;
  • full exact-head PR Quick is green;
  • PR remains unmerged until explicit authorization.

Follow-up slices

After this slice is reviewed, #264 continues with:

  1. concurrency + queue state-preserving policy updates;
  2. rate + partition state-preserving policy updates.

Refs #264, #273, #324, #328, #322, #323, #319.

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