Skip to content

feat(server): pre-call connection admission bounds (issue #162) - #209

Merged
SunSi12138 merged 4 commits into
devfrom
feature/issue-162-connection-admission
Aug 16, 2026
Merged

SunSi12138 merged 4 commits into
devfrom
feature/issue-162-connection-admission

Conversation

@SunSi12138

Copy link
Copy Markdown
Owner

feat(server): pre-call connection admission bounds (issue #162)

Summary

Bounds the framework-owned connection resources that exist before RPC call admission:
every server now has a hard limit on simultaneously live accepted connections and an
independent hard limit on connections still inside TLS / Protocol v2 / authentication
handshake. Over-limit connections are rejected (closed) immediately — they never enter the
handshake/session lifecycle and never spawn a framework task.

Evidence (Phase 0, baremetal 8 pinned cores @ fd236a5)

With no bound, the framework-owned live set grows exactly with arrival rate:

scenario N socket-fd delta alloc delta VmRSS delta Stop time
tcp-stall-nobytes 100 / 1000 / 5000 200 / 2000 / 10000 6.4 / 59.4 / 298 MB 10 / 48 / 203 MB 39 / 54 / 213 ms
tls-ready-stall-protocol 100 / 1000 / 2000 200 / 2000 / 4000 5.6 / 79.6 / 166 MB — 12 / 53 / 86 ms
  • Pre-auth connections are invisible to the existing sharplink.connections.active
    gauge (Ready-only, 0 observed in every stall scenario).
  • TLS handshake burst: 256 concurrent handshakes ≈ 388 ms CPU on 8 cores
    (~1.5 ms CPU per handshake, wall ≈ 117 ms) — an unbound burst can saturate cores.
  • Conclusion: Go — single connection bound would conflate two different resource
    shapes (an idle Ready connection is orders of magnitude cheaper than a TLS handshake),
    so both bounds are exposed; handshakes are additionally always capped by the
    connection bound.

Design

  • Connection slot: acquired synchronously in the accept loop right after
    AcceptAsync; released exactly once at terminal cleanup. A rejected connection is
    disposed inline and the loop continues — no task, no lifecycle entry.
  • Handshake slot: covers TLS AuthenticateAsync + Protocol v2 handshake +
    application authenticator; released exactly once at the Ready transition (or by the
    terminal cleanup for every pre-Ready failure: TLS throw/timeout, protocol reject,
    auth reject/cancel, Stop, duplicate/replaced connection id).
  • Exactly-once ownership: one lease per admitted connection with idempotent
    releases; the single release owner is the terminal cleanup plus the Ready transition
    for the handshake slot. No manual Interlocked.Decrement scattered across catch blocks.
  • No waiting: over-limit connections are rejected immediately. Pre-Ready connections
    carry no user state worth queueing; waiting would turn "handshaking" into "parked and
    still consuming a socket + task", exactly the unbounded growth this issue removes.
  • Not on the request hot path: admission lives only on the accept/handshake path;
    RPC dispatch never touches the gate.
  • Stop/drain: slots release through the existing framework-task drain; Stop and
    Accept races cannot leak a slot (task terminal cleanup always runs).

API (server-specific, mirrors UseAdmissionControl)

builder.UseConnectionAdmission(options =>
{
    options.MaxConcurrentConnections = 10_000;  // default 1024
    options.MaxConcurrentHandshakes = 256;      // default 0 = follows connection bound
});
  • MaxConcurrentHandshakes 0 (default) means no independent handshake bound;
    a positive value must not exceed MaxConcurrentConnections.
  • Distinct from MaxConcurrentCallsPerConnection/PerServer (post-handshake RPC
    dispatch) — documented on the options class.

Diagnostics

  • sharplink.connections.admitted / sharplink.connections.handshakes.active (UpDown)
  • sharplink.connections.rejected with sharplink.admission.reason =
    connection_limit | handshake_limit
  • 5 s-throttled warning log (1105) and startup capacity log (5004).

Tests

21 deterministic tests in ConnectionAdmissionTests:
options validation, gate unit tests (readmit after release, idempotent release),
scripted-listener lifecycle coverage (immediate reject + dispose-exactly-once, TLS
throw, protocol reject + peer error frame, auth reject, Ready handshake-slot release,
handshake bound, Stop races, 2000-connection churn to zero, duplicate connection id,
handshake timeout), rejection telemetry, real-TCP connection bound and TLS handshake
bound behavior. Full unit suite 982/982.

Validation gates

  • Overload: live accepted connections ≤ configured bound; rejected connections leave
    no retained framework task (head evidence matrix on baremetal, default limits).
  • Healthy path: unary load base-vs-head comparison (no hot-path change by construction).
  • quick CI + integration/chaos suites on this PR.

sunsi added 3 commits August 16, 2026 11:26
…handshake hard bounds

Issue #162: bound the framework-owned accepted/live connection set before
RPC call admission. Every accepted connection holds a connection slot from
accept to terminal cleanup; the TLS + Protocol v2 + authentication phase
additionally holds a handshake slot released at Ready or on failure.
Over-limit connections are closed immediately without entering the
handshake/session lifecycle or spawning a framework task.

- SharpLinkConnectionAdmissionOptions (server-specific): MaxConcurrentConnections
  default 1024; MaxConcurrentHandshakes default 0 (follows the connection bound).
- ServerConnectionAdmission gate: interlocked counters, one lease per connection
  with idempotent exactly-once releases; single release owner is the terminal
  cleanup plus the Ready transition for the handshake slot.
- Telemetry: sharplink.connections.admitted / .handshakes.active (updown) and
  sharplink.connections.rejected with reason; throttled 1105 warning and 5004
  startup capacity logs.
- 21 deterministic tests: options validation, gate unit tests, scripted-listener
  lifecycle/race coverage (reject, TLS throw, protocol reject, auth reject,
  Ready release, handshake bound, stop races, 2k churn, duplicate id, timeout),
  rejection telemetry, real-TCP connection and TLS handshake bounds.
…es in evidence probe

- ServerConnectionAdmission.ServerConnectionAdmissionLease -> Lease.
- Evidence probe observes sharplink.connections.admitted/rejected (present on the
  fixed build, zero on the base build) and disposes client transports on rejected
  connects so head runs do not leak sockets into the fd measurements.
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26de539bbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

… cleanup, isolate reject-dispose failures, probe throttle and gauges

- Hold both admission slots until terminal cleanup completes: a slow transport
  disposal can no longer hand the slot to a new connection while the previous
  socket/task is still live (P1). Releases run in a nested finally so a failing
  cleanup still releases both slots.
- A rejected connection whose DisposeAsync throws is logged and skipped instead
  of faulting the whole accept loop (P2).
- Evidence probe: SemaphoreSlim throttle replaces the racy pending counter in
  both connection launchers; AdmittedConnectionsObserved sampled at peak while
  connections are live; scenario-filter argument; document note explains the
  default-bound behavior of head runs.
- 3 new tests: slot held until terminal cleanup completes, dispose failure
  releases slots, rejected dispose failure does not fault the server.
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: c6d3fba435

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@SunSi12138
SunSi12138 merged commit 2d24fcd into dev Aug 16, 2026
3 checks passed
@SunSi12138
SunSi12138 deleted the feature/issue-162-connection-admission branch August 20, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant