Skip to content

Commit f075c79

Browse files
authored
Fix cancellation token reported when using retries (#2345)
1 parent 87cce28 commit f075c79

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ protected void HandleUnexpectedError(Exception ex)
529529
CommitReason commitReason;
530530

531531
// Cancellation token triggered by dispose could throw here.
532-
if (ex is OperationCanceledException && CancellationTokenSource.IsCancellationRequested)
532+
if (ex is OperationCanceledException operationCanceledException && CancellationTokenSource.IsCancellationRequested)
533533
{
534534
// Cancellation could have been caused by an exceeded deadline.
535535
if (IsDeadlineExceeded())
@@ -542,7 +542,21 @@ protected void HandleUnexpectedError(Exception ex)
542542
else
543543
{
544544
commitReason = CommitReason.Canceled;
545-
resolvedCall = CreateStatusCall(Disposed ? GrpcProtocolConstants.CreateDisposeCanceledStatus(ex) : GrpcProtocolConstants.CreateClientCanceledStatus(ex));
545+
Status status;
546+
if (Disposed)
547+
{
548+
status = GrpcProtocolConstants.CreateDisposeCanceledStatus(exception: null);
549+
}
550+
else
551+
{
552+
// Replace the OCE from CancellationTokenSource with an OCE that has the passed in cancellation token if it is canceled.
553+
if (Options.CancellationToken.IsCancellationRequested && Options.CancellationToken != operationCanceledException.CancellationToken)
554+
{
555+
ex = new OperationCanceledException(Options.CancellationToken);
556+
}
557+
status = GrpcProtocolConstants.CreateClientCanceledStatus(ex);
558+
}
559+
resolvedCall = CreateStatusCall(status);
546560
}
547561
}
548562
else

src/Grpc.Net.Client/Internal/Retry/StatusGrpcCall.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,21 @@ public void Dispose()
5656

5757
public Task<TResponse> GetResponseAsync()
5858
{
59-
return Task.FromException<TResponse>(new RpcException(_status));
59+
return CreateErrorTask<TResponse>();
6060
}
6161

6262
public Task<Metadata> GetResponseHeadersAsync()
6363
{
64-
return Task.FromException<Metadata>(new RpcException(_status));
64+
return CreateErrorTask<Metadata>();
65+
}
66+
67+
private Task<T> CreateErrorTask<T>()
68+
{
69+
if (_channel.ThrowOperationCanceledOnCancellation && _status.DebugException is OperationCanceledException ex)
70+
{
71+
return Task.FromException<T>(ex);
72+
}
73+
return Task.FromException<T>(new RpcException(_status));
6574
}
6675

6776
public Status GetStatus()

test/FunctionalTests/Client/CancellationTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,39 @@ async Task<DataMessage> UnaryMethod(DataMessage request, ServerCallContext conte
509509
Assert.AreEqual(StatusCode.Cancelled, call.GetStatus().StatusCode);
510510
}
511511

512+
[Test]
513+
public async Task Unary_Retry_CancellationImmediately_TokenMatchesSource()
514+
{
515+
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
516+
async Task<DataMessage> UnaryMethod(DataMessage request, ServerCallContext context)
517+
{
518+
await tcs.Task;
519+
return new DataMessage();
520+
}
521+
522+
SetExpectedErrorsFilter(writeContext =>
523+
{
524+
return true;
525+
});
526+
527+
// Arrange
528+
var method = Fixture.DynamicGrpc.AddUnaryMethod<DataMessage, DataMessage>(UnaryMethod);
529+
var serviceConfig = ServiceConfigHelpers.CreateRetryServiceConfig();
530+
var channel = CreateChannel(throwOperationCanceledOnCancellation: true, serviceConfig: serviceConfig);
531+
var client = TestClientFactory.Create(channel, method);
532+
533+
// Act
534+
var cts = new CancellationTokenSource();
535+
cts.Cancel();
536+
537+
var call = client.UnaryCall(new DataMessage(), new CallOptions(cancellationToken: cts.Token));
538+
539+
// Assert
540+
var ex = await ExceptionAssert.ThrowsAsync<OperationCanceledException>(() => call.ResponseAsync).DefaultTimeout();
541+
Assert.AreEqual(cts.Token, ex.CancellationToken);
542+
Assert.AreEqual(StatusCode.Cancelled, call.GetStatus().StatusCode);
543+
}
544+
512545
[Test]
513546
public async Task ServerStreaming_CancellationDuringCall_TokenMatchesSource()
514547
{

0 commit comments

Comments
 (0)