Packet Dispatch
On this page
Namespace: Nalix.Runtime.Dispatching
Assembly: Nalix.Runtime
PacketDispatchChannel (public sealed class PacketDispatchChannel : PacketDispatcherBase<IPacket>, IPacketDispatch, IDisposable, IActivatable) is the runtime bridge between transport buffers (IBufferLease) and [PacketHandler] methods. It manages background worker loops, coalesced wake signaling (WorkerWakeSignal), $O(1)$ opcode resolution, PacketRegistry deserialization, and deterministic lease/packet disposal.
Source Mapping
src/Nalix.Runtime/Dispatching/PacketDispatchChannel.cssrc/Nalix.Runtime/Internal/Routing/DispatchChannel.cssrc/Nalix.Runtime/Internal/Routing/WorkerWakeSignal.cssrc/Nalix.Runtime/Dispatching/Options/PacketDispatchOptions.cssrc/Nalix.Runtime/Options/PacketDrainOptions.cs
Runtime Flow
flowchart LR
A["Transport IBufferLease"] --> B["HandlePacket(lease, connection)"]
B --> C["lease.Retain() + _dispatch.PushCore(...)"]
C -->|readyEmitted == true| D["RequestWake() -> WorkerWakeSignal.Set()"]
D --> E["DispatchWorkerLoopAsync: _dispatch.TryClaim(out session)"]
E --> F["Extract OpCode & Options.TryResolveHandler"]
F --> G["PacketRegistry.TryDeserialize / MemoryPacket"]
G --> H["ExecuteResolvedHandlerAsync (Middleware + Handler)"]
H --> I["Dispose PacketContext + Dispose lease"]HandlePacket Handoff Semantics
HandlePacket(IBufferLease lease, IConnection connection) executes on the transport receive thread:
- Disposes
leaseand returns immediately ifleaseorconnectionisnull. - If
connection.TCP.Framing == TransportFraming.UInt16LengthPrefixedandlease.Length < PacketConstants.HeaderSize(6bytes), disposeslease, callsconnection.IncrementErrorCount(), and returns. - Calls
lease.Retain()to increment the buffer’s reference count before asynchronous handoff. - Enqueues via
_dispatch.PushCore(connection, lease, out bool readyEmitted, noBlock: true). If the queue rejects the packet (full or connection closed), disposes the retainedleaseand increments_totalDropped. - If
readyEmittedistrue(the connection just became claimable), callsRequestWake()to wake one parked worker.
Worker Loop Sizing & CPU Affinity (Activate)
Activate(CancellationToken) schedules worker loops through TaskManager.ScheduleWorker (net.dispatch.process.{i} in group net/dispatch with WorkerPriority.HIGH). The number of loops (_dispatchLoops) is resolved in priority order:
| Condition | Resolved _dispatchLoops | Execution Mode |
|---|---|---|
Options.Drain.DispatchProcessorAffinities is non-empty (e.g. "0,2,4,6") | Number of parsed core indices (affinityCores.Length) | Dedicated OS thread (OSPriority = ThreadPriority.Normal) pinned to affinityCores[i] |
Options.Drain.Count > 0 (set via WithDispatchLoopCount(n) or INI) | Options.Drain.Count | Async ThreadPool worker |
Default (Options.Drain.Count == 0 and no affinities) | Math.Clamp(Environment.ProcessorCount, Options.Drain.MinDispatchLoops, Options.Drain.MaxDispatchLoops) | Async ThreadPool worker |
Coalesced Wake & Drain Mechanics
Each worker owns an allocation-free WorkerWakeSignal (backed by ManualResetValueTaskSourceCore<bool>) and an atomic parked flag (_parked[i]):
- Drain Budget (
_maxDrainPerWake): Calculated in the constructor as:Math.Clamp( Environment.ProcessorCount * Options.Drain.MaxDrainPerWakeMultiplier, Options.Drain.MinDrainPerWake, Options.Drain.MaxDrainPerWake) - Burst Fan-Out: When a worker claims a connection (
_dispatch.TryClaim(out session)) and observes_dispatch.HasClaimableConnection == true, it immediately callsRequestWake()so another parked worker wakes up in parallel. - Lost-Wake Prevention: Before parking, a worker sets
_parked[index] = 1viaInterlocked.Exchange, spins briefly (16iterations ofThread.SpinWait(8)), and re-checks_dispatch.HasClaimableConnectionbefore awaitingsignal.WaitAsync().
Execution and Disposal Guarantees (ExecutePacketAsync)
For each dequeued IBufferLease:
- OpCode Extraction: Calls
connection.PacketClassifier.Extract(lease.Span)and readslease.IsReliableandlease.EncryptedOnWire. - Early Handler Lookup: Calls
Options.TryResolveHandler(opcode, out handler)before deserializing. If no handler is registered foropcode, logs a throttled warning, disposeslease, incrementsconnection.IncrementErrorCount(), and skips deserialization entirely. - Deserialization /
MemoryPacketFast Path:- If
handler.ExpectedPacketType == typeof(MemoryPacket), wrapslease.Memorydirectly in aMemoryPacketwithout callingPacketRegistry. - Otherwise calls
PacketRegistry.TryDeserialize(lease.Span, out IPacket? deserialized). On failure, increments_deserializationErrors, disposeslease, and incrementsconnection.IncrementErrorCount().
- If
- Synchronous vs. Asynchronous Completion:
- Calls
Options.ExecuteResolvedHandlerAsync(in handler, packet, connection, isReliable, encryptedOnWire, ct). - If the
ValueTaskcompletes synchronously (IsCompletedSuccessfully), disposesleaseimmediately on the worker loop with zero continuation allocation. - If the handler awaits asynchronous I/O, rents a pooled
DispatchContinuationfromObjectPoolManager.Shared(workItem.Initialize(connection, lease, pending.AsTask(), ct)) so the worker loop is not blocked andleaseis disposed when the task finishes.
- Calls
Diagnostics (IReportable)
GenerateReport() and WriteReportData(Utf8JsonWriter) emit:
- Worker & Channel Metrics:
Running,DispatchLoops,WakeSignals,WakeReads,IdleWorkers,TotalPackets,PeakPackets,TotalConnections,ReadyConnections, andPendingPerPriority(URGENTdown toNONE). - Pipeline Metrics (
Options.Metrics):ActiveExecutions,TotalExecutions,TotalDropped(_totalDropped + _dispatch.TotalEvicted),TotalErrors,DeserializationErrors,AverageTimeMs,MaxTimeMs, and per-middleware breakdowns (MiddlewareMetrics).