using System.IO.Pipes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Contracts; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Metrics; using ZB.MOM.WW.MxGateway.Server.Workers; namespace ZB.MOM.WW.MxGateway.Server.Sessions; /// Factory for creating worker clients and launching worker processes. public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory { /// /// Kernel buffer quota requested for each direction of a worker pipe. A zero quota — what the /// short 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. /// private const int PipeBufferSizeBytes = 128 * 1024; private readonly IWorkerProcessLauncher _workerProcessLauncher; private readonly GatewayMetrics _metrics; private readonly TimeProvider _timeProvider; private readonly ILoggerFactory _loggerFactory; private readonly GatewayOptions _options; /// Initializes a new instance of the SessionWorkerClientFactory class. /// Service for launching worker processes. /// Configuration options. /// Metrics collector for gateway events. /// Logger factory for creating loggers. /// Optional time provider for testing; defaults to system time. public SessionWorkerClientFactory( IWorkerProcessLauncher workerProcessLauncher, IOptions options, GatewayMetrics metrics, ILoggerFactory loggerFactory, TimeProvider? timeProvider = null) { _workerProcessLauncher = workerProcessLauncher ?? throw new ArgumentNullException(nameof(workerProcessLauncher)); ArgumentNullException.ThrowIfNull(options); _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _timeProvider = timeProvider ?? TimeProvider.System; _options = options.Value; } /// public async Task CreateAsync( GatewaySession session, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(session); NamedPipeServerStream? pipe = CreatePipe(session.PipeName); WorkerProcessHandle? processHandle = null; IWorkerClient? workerClient = null; using CancellationTokenSource startupCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); startupCancellation.CancelAfter(session.StartupTimeout); try { session.TransitionTo(SessionState.StartingWorker); processHandle = await _workerProcessLauncher .LaunchAsync( new WorkerProcessLaunchRequest( session.SessionId, session.PipeName, GatewayContractInfo.WorkerProtocolVersion, session.Nonce, pipe), startupCancellation.Token) .ConfigureAwait(false); session.TransitionTo(SessionState.WaitingForPipe); await WaitForPipeConnectionAsync(pipe, startupCancellation.Token).ConfigureAwait(false); session.TransitionTo(SessionState.Handshaking); WorkerFrameProtocolOptions frameOptions = new( session.SessionId, GatewayContractInfo.WorkerProtocolVersion, _options.Worker.MaxMessageBytes); WorkerClientConnection connection = new( session.SessionId, session.Nonce, pipe, frameOptions, processHandle); WorkerClientOptions clientOptions = new() { HeartbeatGrace = TimeSpan.FromSeconds(_options.Worker.HeartbeatGraceSeconds), HeartbeatCheckInterval = TimeSpan.FromSeconds(_options.Worker.HeartbeatIntervalSeconds), EventChannelCapacity = _options.Events.QueueCapacity, MaxPendingCommands = _options.Sessions.MaxPendingCommandsPerSession, }; workerClient = new WorkerClient( connection, clientOptions, _metrics, _timeProvider, _loggerFactory.CreateLogger()); pipe = null; processHandle = null; session.TransitionTo(SessionState.InitializingWorker); await workerClient.StartAsync(startupCancellation.Token).ConfigureAwait(false); return workerClient; } catch (Exception exception) { if (workerClient is not null) { try { workerClient.Kill("OpenSessionFailed"); } catch { // Preserve the startup failure while still disposing below. } await workerClient.DisposeAsync().ConfigureAwait(false); } else { if (processHandle is not null) { try { if (!processHandle.Process.HasExited) { processHandle.Process.Kill(entireProcessTree: true); _metrics.WorkerKilled("OpenSessionFailed"); } } finally { processHandle.Dispose(); } } pipe?.Dispose(); } if (exception is OperationCanceledException && startupCancellation.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { throw new TimeoutException( $"Worker session {session.SessionId} did not complete startup within {session.StartupTimeout}.", exception); } throw; } } /// Creates a named pipe for worker communication. /// The pipe name. /// Named pipe server stream. /// /// The buffer sizes are explicit so the pipe is not created with a zero quota; see /// . On Unix hosts (the macOS test matrix, where named pipes /// are Unix domain sockets) the sizes are advisory — the fix targets Windows production. /// private static NamedPipeServerStream CreatePipe(string pipeName) { return new NamedPipeServerStream( pipeName, PipeDirection.InOut, maxNumberOfServerInstances: 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: PipeBufferSizeBytes, outBufferSize: PipeBufferSizeBytes); } /// Waits for a client to connect to the pipe. /// The named pipe. /// Cancellation token. private static async Task WaitForPipeConnectionAsync( NamedPipeServerStream pipe, CancellationToken cancellationToken) { await pipe.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); } }