perf(ipc): WaitAsync command timeouts + forward WorkerCancel so a timed-out COM call frees the STA
This commit is contained in:
@@ -159,7 +159,11 @@ public sealed class WorkerClientTests
|
||||
CreateCommand(MxCommandKind.GetWorkerInfo),
|
||||
TestTimeout,
|
||||
CancellationToken.None);
|
||||
WorkerEnvelope secondCommand = await pipePair.WorkerReader.ReadAsync().AsTask().WaitAsync(TestTimeout);
|
||||
|
||||
// The timeout also emits a WorkerCancel for the abandoned correlation (GWC-31), which sits
|
||||
// ahead of the second command on the FIFO pipe; skip it rather than mistaking it for the
|
||||
// command this assertion is about.
|
||||
WorkerEnvelope secondCommand = await ReadNextCommandAsync(pipePair);
|
||||
await pipePair.WriteAsync(
|
||||
CreateCommandReplyEnvelope(secondCommand.CorrelationId, MxCommandKind.GetWorkerInfo));
|
||||
|
||||
@@ -169,6 +173,47 @@ public sealed class WorkerClientTests
|
||||
Assert.Equal(MxCommandKind.GetWorkerInfo, reply.Reply.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A command timeout abandons the gateway-side wait, but the worker keeps the correlation on
|
||||
/// its single STA queue and would still run it — so the gateway forwards a <c>WorkerCancel</c>
|
||||
/// for the abandoned correlation id (GWC-31). Without it, a client that retries after a
|
||||
/// timeout stacks work the worker still intends to execute. Asserted on the wire because the
|
||||
/// cancel is protocol behavior the worker depends on, not an internal detail.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WhenCommandTimesOut_SendsWorkerCancelForThatCorrelation()
|
||||
{
|
||||
await using PipePair pipePair = await PipePair.CreateAsync();
|
||||
await using WorkerClient client = CreateClient(pipePair);
|
||||
await CompleteHandshakeAsync(client, pipePair);
|
||||
|
||||
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
||||
CreateCommand(MxCommandKind.Ping),
|
||||
TimeSpan.FromMilliseconds(50),
|
||||
CancellationToken.None);
|
||||
|
||||
WorkerEnvelope commandEnvelope = await pipePair.WorkerReader.ReadAsync().AsTask().WaitAsync(TestTimeout);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, commandEnvelope.BodyCase);
|
||||
|
||||
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
|
||||
async () => await invokeTask.WaitAsync(TestTimeout));
|
||||
Assert.Equal(WorkerClientErrorCode.CommandTimeout, exception.ErrorCode);
|
||||
|
||||
WorkerEnvelope cancelEnvelope = await pipePair.WorkerReader.ReadAsync().AsTask().WaitAsync(TestTimeout);
|
||||
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCancel, cancelEnvelope.BodyCase);
|
||||
Assert.Equal(commandEnvelope.CorrelationId, cancelEnvelope.CorrelationId);
|
||||
Assert.False(string.IsNullOrWhiteSpace(cancelEnvelope.WorkerCancel.Reason));
|
||||
Assert.True(
|
||||
cancelEnvelope.Sequence > commandEnvelope.Sequence,
|
||||
$"The cancel arrived with sequence {cancelEnvelope.Sequence} after {commandEnvelope.Sequence}; "
|
||||
+ "envelope sequences must be strictly increasing in wire order.");
|
||||
|
||||
// The timeout fails one command; it is not a session fault.
|
||||
Assert.Equal(WorkerClientState.Ready, client.State);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The envelope <c>sequence</c> is a monotonic per-sender counter (gateway.md), so the values
|
||||
/// observed on the pipe must be strictly increasing in wire order. Stamping the sequence when
|
||||
@@ -1030,6 +1075,24 @@ public sealed class WorkerClientTests
|
||||
return envelope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads gateway envelopes until a <c>WorkerCommand</c> arrives, skipping the control envelopes
|
||||
/// (such as the <c>WorkerCancel</c> a command timeout emits) that may precede it on the FIFO pipe.
|
||||
/// </summary>
|
||||
/// <param name="pipePair">The connected pipe pair whose worker side is read.</param>
|
||||
/// <returns>The next command envelope written by the gateway.</returns>
|
||||
private static async Task<WorkerEnvelope> ReadNextCommandAsync(PipePair pipePair)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
WorkerEnvelope envelope = await pipePair.WorkerReader.ReadAsync().AsTask().WaitAsync(TestTimeout);
|
||||
if (envelope.BodyCase == WorkerEnvelope.BodyOneofCase.WorkerCommand)
|
||||
{
|
||||
return envelope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitUntilAsync(
|
||||
Func<bool> predicate,
|
||||
TimeSpan timeout)
|
||||
|
||||
Reference in New Issue
Block a user