Server 3.2.0: connection context, targeted events, one ordered outbound queue per connection - #3
Merged
Conversation
…nd queue per connection What a shared WitServer needs before it can serve many clients that must not see each other's events (the WitCloud /worker/v2 case: one endpoint for every node instead of one server per node). Additive on the server package only; nothing on the wire, in the core, the client packages or the DI package; the defaults reproduce 3.1 exactly. - ConnectionContext.Current: an AsyncLocal set around every request (inside ProcessMessage, so it follows the service method through its awaits and into tasks it starts) and around the authorization handshake - the connection id, the server, the transport and the principal. The principal lives on ConnectionInfo, established once at authorization through the opt-in IConnectionAuthenticator; IAccessTokenValidator is untouched. IConnectionContextAccessor / ConnectionContextAccessor for injection, BeginScope for unit tests of services. - CallbackScope.Target(id | ids) / TargetCaller(): the target of an event raise, read in WitServer.OnCallback - the raise chain is synchronous, so neither processor nor IRequestProcessor changes. A report per server and connection (Sent, UnknownConnection, NotAuthorized, QueueFull, SendFailed, SendTimedOut, Refused), Completion once every accepted send finished. TargetedOnly mode refuses an untargeted raise. - ConnectionOutbox: one ordered queue and one writer per connection for responses, handshake replies and callbacks alike, replacing the fire-and-forget send task per callback per connection. Wire order is a stated guarantee (a callback raised inside a handler precedes the handler's response), the AEAD counter advances in wire order by construction, a stuck transport holds only its own queue. CallbackDeliveryOptions bound the queue per connection (messages, bytes) with Log (default, = 3.1) / CloseConnection / DropNewest. - 46 new tests (ConnectionContextTests, TargetedCallbackTests on Pipes / WebSocket / TCP, ConnectionOutboxTests on a stub transport that can block a write); the suite is 912/912 on net10.0-windows and net8.0-windows. - OutWit.Communication.LoadTests: a console harness (not in the CI gate) that runs one shared server with targeted callbacks against N per-client servers and the pre-3.2 broadcast, with a throttled or stalled socket for the slow-neighbour case; numbers in the PR and in WitCloud/@Docs/Roadmap. - README (server), CHANGELOG, ROADMAP-v3, MIGRATION-3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
OutWit.Communication.Server3.1.1 → 3.2.0: what a sharedWitServerneeds before it can serve many clients that must not see each other's events (the WitCloud/worker/v2case: one endpoint for every node instead of one server per node). Planned inWitCloud/@Docs/Roadmap/plan-witrpc-connection-context-and-targeted-callbacks.md; additive on the server package only.Connection context for a service
ConnectionContext.Current— anAsyncLocalthe server sets insideProcessMessage(visible to the validator, the processor, the service method and whatever it awaits or starts; gone when the request completes) and around the authorization handshake. CarriesConnectionId,ServerId/ServerName/Transport,Principal,AuthorizedAtUtc.ConnectionInfo, established once at authorization through the opt-inIConnectionAuthenticator(TryAuthenticate(token, out ClaimsPrincipal?));IAccessTokenValidatoris untouched and a validator that does not opt in leaves it null.IConnectionContextAccessor/ConnectionContextAccessorfor injection,ConnectionContext.BeginScope(...)for unit tests of services.Targeted events
CallbackScope.Target(connectionId)/Target(connectionIds)/TargetCaller()around an ordinary C# event raise.WitServer.OnCallbackreads the target when the callback reaches it — the raise chainevent → HandleEvent → Callback → OnCallbackis synchronous, so neither processor norIRequestProcessorchanges.Reportnow,Completiononce every accepted send finished):Sent,UnknownConnection,NotAuthorized,QueueFull,SendFailed,SendTimedOut,Refused. A service registered in two servers is raised in both; the one owning the connection delivers, the other reports the id unknown.WithTargetedCallbacksOnly()refuses an untargeted raise (logged,Refused). Without a scope the server broadcasts exactly as before.One ordered outbound queue per connection
ConnectionOutbox: one queue and one writer task per connection for responses, handshake replies and callbacks alike, replacing the fire-and-forget send task per callback per connection. Wire order is a stated guarantee: a callback raised inside a handler precedes the handler's response; the AEAD counter advances in wire order by construction; a stuck transport holds only its own queue.WithCallbackDelivery(...)/CallbackDeliveryOptions:MaxPendingCallbacks,MaxPendingCallbackBytes(0 = unbounded),OverflowPolicy=Log(default: queue anyway, warn, keep the connection — the 3.1 behaviour) /CloseConnection(refuse, close; a callback send timeout closes too) /DropNewest. Responses are never counted and never refused. This is the outbound half of the 2026-08-30 audit's P0.2; the inbound bound stays for its Stage 1.WitServer.GetPendingCallbacks(connectionId),AuthorizedConnectionCount,ConnectionInfo.PendingCallbacks/PendingCallbackBytes.Compatibility
Nothing on the wire, nothing in the core, the client packages, Blazor, REST, InterProcess or the DI package (its floor stays
Server >= 3.1.1; a consumer takes 3.2.0 with an explicit pin and behaves as before until it opens a scope, reads the context or sets delivery options). The oldWitServerconstructor stays; a new overload takesCallbackDeliveryOptions.Tests
OutWit.Communication.Testsnet10.0-windows (CI filter, 46 new)OutWit.Communication.Testsnet8.0-windowsOutWit.InterProcess.Tests(real agent processes)New fixtures:
ConnectionContextTests(Pipes / WebSocket / TCP: id per connection, flows across awaits, no leak across 6 clients × 20 concurrent calls, principal from the authenticator, null for a plain validator, refusal closes),TargetedCallbackTests(only the target / a set / broadcast unchanged / unknown id / targeted-only / 200 in order / shared service in two servers),ConnectionOutboxTestson a stub transport that can block a write (callback-before-response, 1000 in order, responses and callbacks in one order, slow neighbour, disconnect mid-send, queue-full close only the offender, Log keeps queuing, DropNewest, byte budget, responses never counted, send timeout close vs warn, dispose with pending sends, targeted-only refusal, per-connection report).Load harness
Communication/OutWit.Communication.LoadTests(console, inOutWit.slnx, not in the CI gate): one shared server with targeted callbacks vs N per-client servers vs the pre-3.2 broadcast, 100 / 300 / 1000 nodes over real WebSocket on loopback, a throttled or stalled socket for the slow-neighbour case. Numbers below and inWitCloud/@Docs/Roadmap/baseline-2026-09-07-witrpc-load.md.Ryzen 9 5950X, one process (server + N
WitClients), WebSocket over loopback, JSON, AES-GCM, 256 B payload, 20 s steady dispatch, dispatch → ack = callback down + request up:Slow neighbour (node 7's socket 200 ms per write, or never draining), the other 99 nodes: shared
Log0.43 / 0.62 / 2.90 ms, sharedCloseConnection(bound 50, send timeout 2 s) 0.42 / 0.67 / 3.06 ms, per-client 0.41 / 0.73 / 3.76 ms — all at the no-slow-node baseline (0.40 / 0.66 / 2.98 ms); the stuck frames accumulate only in the slow node's own queue (105–199). UnderCloseConnectionthe stalled node is closed 2 s after its first stuck frame and every later task to it reportsUnknownConnectionat once (the directory's cue to requeue); underLog(= 3.1) the frames sit in that queue and the connection stays. The queued-callbacks maximum over healthy nodes was 1 in every targeted run.Docs
Server README ("Connection context and targeted events (3.2)"), CHANGELOG (
Server 3.2.0), ROADMAP-v3 ("3.2.0 follow-up"), MIGRATION-3 (Step 4: no consumer action).🤖 Generated with Claude Code