feat(server): add MxGateway:Sessions:WorkerReadyWaitTimeoutMs (default off)

Adds WorkerReadyWaitTimeoutMs to SessionOptions (default 0 = disabled),
validates >= 0 in GatewayOptionsValidator, documents it in
GatewayConfiguration.md, and adds validator + default-value tests.
No wait/poll logic is implemented here (that is Task 8).
This commit is contained in:
Joseph Doherty
2026-06-16 16:38:31 -04:00
parent 1cfad83c06
commit ea17528767
5 changed files with 57 additions and 1 deletions
@@ -185,6 +185,10 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBase<GatewayOption
options.DetachGraceSeconds,
"MxGateway:Sessions:DetachGraceSeconds must be zero or greater (0 disables detach-grace retention).",
builder);
AddIfNegative(
options.WorkerReadyWaitTimeoutMs,
"MxGateway:Sessions:WorkerReadyWaitTimeoutMs must be greater than or equal to zero.",
builder);
// NOTE: We intentionally do NOT reject !AllowMultipleEventSubscribers &&
// MaxEventSubscribersPerSession > 1 as a hard validation error here. The default
@@ -56,4 +56,15 @@ public sealed class SessionOptions
/// effectively 1 when it is <see langword="false"/>. Must be greater than zero.
/// </summary>
public int MaxEventSubscribersPerSession { get; init; } = 8;
/// <summary>
/// Gets the bounded time, in milliseconds, the gateway will wait for a worker client
/// to reach <c>Ready</c> when the session itself is already <c>Ready</c> but the worker
/// state has transiently diverged (e.g. <c>Handshaking</c> after a heartbeat blip).
/// The wait applies only to transient worker states; terminal states
/// (<c>Faulted</c>/<c>Closing</c>/<c>Closed</c>/no worker) fail fast immediately.
/// A value of <c>0</c> (the default) disables the wait — the gateway keeps the original
/// fail-fast behavior. Must be greater than or equal to zero.
/// </summary>
public int WorkerReadyWaitTimeoutMs { get; init; }
}
@@ -37,6 +37,7 @@ public sealed class GatewayOptionsTests
Assert.Equal(30, options.Sessions.DetachGraceSeconds);
Assert.False(options.Sessions.AllowMultipleEventSubscribers);
Assert.Equal(8, options.Sessions.MaxEventSubscribersPerSession);
Assert.Equal(0, options.Sessions.WorkerReadyWaitTimeoutMs);
Assert.Equal(10_000, options.Events.QueueCapacity);
Assert.Equal(EventBackpressurePolicy.FailFast, options.Events.BackpressurePolicy);
@@ -88,6 +89,7 @@ public sealed class GatewayOptionsTests
[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).")]
[InlineData("MxGateway:Sessions:WorkerReadyWaitTimeoutMs", "-1", "MxGateway:Sessions:WorkerReadyWaitTimeoutMs must be greater than or equal to zero.")]
[InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")]
[InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")]
[InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")]
@@ -356,4 +356,41 @@ public sealed class GatewayOptionsValidatorTests
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
Assert.True(result.Succeeded);
}
// -------------------------------------------------------------------------
// WorkerReadyWaitTimeoutMs validation
// -------------------------------------------------------------------------
[Fact]
public void Validate_Fails_WhenWorkerReadyWaitTimeoutMsIsNegative()
{
GatewayOptions options = CloneWithSessions(
ValidOptions(),
new SessionOptions { WorkerReadyWaitTimeoutMs = -1 });
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains(
result.Failures!,
f => f.Contains("MxGateway:Sessions:WorkerReadyWaitTimeoutMs"));
}
[Fact]
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsZero()
{
GatewayOptions options = CloneWithSessions(
ValidOptions(),
new SessionOptions { WorkerReadyWaitTimeoutMs = 0 });
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
Assert.True(result.Succeeded);
}
[Fact]
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsPositive()
{
GatewayOptions options = CloneWithSessions(
ValidOptions(),
new SessionOptions { WorkerReadyWaitTimeoutMs = 5000 });
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
Assert.True(result.Succeeded);
}
}