perf(ipc): give worker pipes real OS buffers instead of zero-quota rendezvous

This commit is contained in:
Joseph Doherty
2026-08-15 12:02:43 -04:00
parent 9958f80026
commit da8463534b
2 changed files with 31 additions and 1 deletions
+11
View File
@@ -164,6 +164,17 @@ class drains both queues to empty, and the heartbeat loop guarantees one
arrives within a heartbeat interval, so worst-case residency is a few envelope
references for seconds — not a leak.
## Pipe Buffers
The gateway creates each worker pipe with an explicit 128 KiB kernel buffer per
direction (`SessionWorkerClientFactory.PipeBufferSizeBytes`) rather than the zero
quota the short `NamedPipeServerStream` overloads request. A zero-quota byte-mode
pipe makes every write rendezvous with a pending read, so a writer with no reader
parked blocks until one arrives — the failure class behind the historical windev
full-suite wedge. A real quota decouples writer latency from reader scheduling and
lets the flush coalescing above actually pay off. On Unix hosts, where named pipes
are Unix domain sockets, the sizes are advisory.
## Verification
The frame protocol lives in `ZB.MOM.WW.MxGateway.Worker.Ipc` (`WorkerFrameReader`,
@@ -12,6 +12,18 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
/// <summary>Factory for creating worker clients and launching worker processes.</summary>
public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
{
/// <summary>
/// Kernel buffer quota requested for each direction of a worker pipe. A zero quota — what the
/// short <see cref="NamedPipeServerStream"/> overloads request — makes every byte-mode write
/// rendezvous with a pending read, so writer latency is coupled to reader scheduling and a
/// writer with no reader parked blocks indefinitely. That is the failure class behind the
/// historical windev full-suite wedge (all tests reported, testhost never exiting). A real
/// quota lets a whole frame land in the kernel and the writer return. 128 KiB comfortably
/// holds the control traffic and typical event batches without reserving nonpaged pool per
/// session for the rare maximum-sized frame, which still streams through in chunks.
/// </summary>
private const int PipeBufferSizeBytes = 128 * 1024;
private readonly IWorkerProcessLauncher _workerProcessLauncher;
private readonly GatewayMetrics _metrics;
private readonly TimeProvider _timeProvider;
@@ -155,6 +167,11 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
/// <summary>Creates a named pipe for worker communication.</summary>
/// <param name="pipeName">The pipe name.</param>
/// <returns>Named pipe server stream.</returns>
/// <remarks>
/// The buffer sizes are explicit so the pipe is not created with a zero quota; see
/// <see cref="PipeBufferSizeBytes"/>. On Unix hosts (the macOS test matrix, where named pipes
/// are Unix domain sockets) the sizes are advisory — the fix targets Windows production.
/// </remarks>
private static NamedPipeServerStream CreatePipe(string pipeName)
{
return new NamedPipeServerStream(
@@ -162,7 +179,9 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
PipeOptions.Asynchronous,
inBufferSize: PipeBufferSizeBytes,
outBufferSize: PipeBufferSizeBytes);
}
/// <summary>Waits for a client to connect to the pipe.</summary>