diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs index 05a6967..2d18295 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs @@ -225,6 +225,10 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBaseThe timeout in milliseconds for connecting to the worker pipe. public int PipeConnectAttemptTimeoutMilliseconds { get; init; } = 2000; + /// + /// Bounded wait, in milliseconds, the worker holds a WriteSecured/WriteSecured2 + /// reply for the matching MXAccess OnWriteComplete callback so the reply's + /// statuses carry the real commit outcome. 0 disables the wait. Deployments + /// raising this above consumer write-timeout budgets (e.g. OtOpcUa's 2 s Tier A + /// write resilience timeout) must raise those in step. + /// + public int WriteCompletionWaitMilliseconds { get; init; } = 1500; + /// The maximum time in seconds for graceful shutdown. public int ShutdownTimeoutSeconds { get; init; } = 10; diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs index cab6fe1..b74f85a 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs @@ -21,6 +21,14 @@ public sealed class WorkerProcessLauncher : IWorkerProcessLauncher public const string WorkerPipeConnectAttemptTimeoutEnvironmentVariableName = "MXGATEWAY_WORKER_PIPE_CONNECT_ATTEMPT_TIMEOUT_MS"; + /// + /// Conveys MxGateway:Worker:WriteCompletionWaitMilliseconds to the worker: + /// the bounded wait for the OnWriteComplete callback on + /// WriteSecured/WriteSecured2 replies. 0 disables the wait. + /// + public const string WorkerWriteCompletionWaitEnvironmentVariableName = + "MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS"; + private readonly IWorkerProcessFactory _processFactory; private readonly IWorkerStartupProbe _startupProbe; private readonly GatewayMetrics _metrics; @@ -175,6 +183,8 @@ public sealed class WorkerProcessLauncher : IWorkerProcessLauncher startInfo.Environment[WorkerNonceEnvironmentVariableName] = request.Nonce; startInfo.Environment[WorkerPipeConnectAttemptTimeoutEnvironmentVariableName] = _workerOptions.PipeConnectAttemptTimeoutMilliseconds.ToString(System.Globalization.CultureInfo.InvariantCulture); + startInfo.Environment[WorkerWriteCompletionWaitEnvironmentVariableName] = + _workerOptions.WriteCompletionWaitMilliseconds.ToString(System.Globalization.CultureInfo.InvariantCulture); commandLine = new WorkerProcessCommandLine(executablePath, arguments); diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs index e05e93b..1506edc 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs @@ -41,6 +41,7 @@ public sealed class GatewayOptionsTests Assert.Equal(3, options.Worker.StartupProbeRetryAttempts); Assert.Equal(250, options.Worker.StartupProbeRetryDelayMilliseconds); Assert.Equal(2000, options.Worker.PipeConnectAttemptTimeoutMilliseconds); + Assert.Equal(1500, options.Worker.WriteCompletionWaitMilliseconds); Assert.Equal(10, options.Worker.ShutdownTimeoutSeconds); Assert.Equal(5, options.Worker.HeartbeatIntervalSeconds); Assert.Equal(15, options.Worker.HeartbeatGraceSeconds); @@ -106,6 +107,7 @@ public sealed class GatewayOptionsTests [InlineData("MxGateway:Worker:ExecutablePath", "worker.dll", "MxGateway:Worker:ExecutablePath must point to a .exe file.")] [InlineData("MxGateway:Worker:StartupProbeRetryAttempts", "0", "MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.")] [InlineData("MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds", "0", "MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.")] + [InlineData("MxGateway:Worker:WriteCompletionWaitMilliseconds", "-1", "MxGateway:Worker:WriteCompletionWaitMilliseconds must be greater than or equal to zero.")] [InlineData("MxGateway:Sessions:DefaultLeaseSeconds", "0", "MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.")] [InlineData("MxGateway:Sessions:LeaseSweepIntervalSeconds", "0", "MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.")] [InlineData("MxGateway:Sessions:DetachGraceSeconds", "-1", "MxGateway:Sessions:DetachGraceSeconds must be zero or greater (0 disables detach-grace retention).")] diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs index f9d0e43..52b3756 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs @@ -43,6 +43,10 @@ public sealed class WorkerProcessLauncherTests "2000", processFactory.LastStartInfo.Environment[ WorkerProcessLauncher.WorkerPipeConnectAttemptTimeoutEnvironmentVariableName]); + Assert.Equal( + "1500", + processFactory.LastStartInfo.Environment[ + WorkerProcessLauncher.WorkerWriteCompletionWaitEnvironmentVariableName]); Assert.DoesNotContain(Nonce, handle.CommandLine.ToString(), StringComparison.Ordinal); Assert.DoesNotContain(Nonce, string.Join(" ", handle.CommandLine.Arguments), StringComparison.Ordinal); Assert.False(pipeReservation.DisposeCalled);