fix(worker): reply WorkerUnavailable to commands that race shutdown (#182)
TryStartCommandTask returned silently when command intake had stopped, so a command refused during shutdown got no reply and no log and the gateway's correlation wait ran out its own timeout. The refusal now writes a WorkerUnavailable reply for the correlation and logs WorkerCommandRejectedAfterShutdown. StopAcceptingCommands is internal so the test can reach the guard (the serial read loop cannot race it today). Test: WorkerPipeSessionTests.CommandAfterShutdown_RepliesWorkerUnavailable.
This commit is contained in:
@@ -1943,6 +1943,60 @@ public sealed class WorkerPipeSessionTests
|
||||
&& (string?)correlationId == "command-dropped-after-shutdown");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A command that reaches the dispatcher after shutdown has stopped command intake must be
|
||||
/// answered with <see cref="ProtocolStatusCode.WorkerUnavailable"/> (and logged) rather than
|
||||
/// dropped silently, which would leave the gateway's correlation wait to run out its own
|
||||
/// timeout with no trace (#182).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CommandAfterShutdown_RepliesWorkerUnavailable()
|
||||
{
|
||||
using CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(10));
|
||||
using PipePair pipePair = await PipePair.CreateAsync(cancellation.Token);
|
||||
FakeRuntimeSession runtime = new();
|
||||
RecordingWorkerLogger logger = new();
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
WorkerPipeSession session = new(
|
||||
new WorkerFrameReader(pipePair.WorkerStream, options),
|
||||
new WorkerFrameWriter(pipePair.WorkerStream, options),
|
||||
options,
|
||||
() => 1234,
|
||||
new WorkerPipeSessionOptions
|
||||
{
|
||||
HeartbeatInterval = TimeSpan.FromSeconds(1),
|
||||
HeartbeatGrace = TimeSpan.FromSeconds(5),
|
||||
},
|
||||
() => runtime,
|
||||
logger);
|
||||
Task runTask = session.RunAsync(cancellation.Token);
|
||||
await CompleteGatewayHandshakeAsync(pipePair, cancellation.Token);
|
||||
|
||||
session.StopAcceptingCommands();
|
||||
await pipePair.GatewayWriter.WriteAsync(
|
||||
CreateCommandEnvelope("command-after-shutdown"),
|
||||
cancellation.Token);
|
||||
|
||||
WorkerEnvelope reply = await ReadUntilAsync(
|
||||
pipePair.GatewayReader,
|
||||
WorkerEnvelope.BodyOneofCase.WorkerCommandReply,
|
||||
envelope => envelope.CorrelationId == "command-after-shutdown",
|
||||
cancellation.Token);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.WorkerUnavailable, reply.WorkerCommandReply.Reply.ProtocolStatus.Code);
|
||||
Assert.Equal(MxCommandKind.Register, reply.WorkerCommandReply.Reply.Kind);
|
||||
Assert.Equal(SessionId, reply.WorkerCommandReply.Reply.SessionId);
|
||||
Assert.False(runtime.DispatchStarted.IsSet);
|
||||
Assert.Contains(
|
||||
logger.Events,
|
||||
entry => entry.EventName == "WorkerCommandRejectedAfterShutdown"
|
||||
&& entry.Fields.TryGetValue("correlation_id", out object? correlationId)
|
||||
&& (string?)correlationId == "command-after-shutdown");
|
||||
|
||||
await SendShutdownAndWaitAsync(pipePair, runTask, cancellation.Token);
|
||||
}
|
||||
|
||||
private static WorkerPipeSession CreateSession(
|
||||
Stream inbound,
|
||||
Stream outbound,
|
||||
|
||||
Reference in New Issue
Block a user