public override void Complete() { switch (_state) { case InternalState.IncompleteBufferWriter: _state = InternalState.CompleteBufferWriter; if (!DirectSerializationSupported) { CompatibilityHelpers.Assert(_bufferWriter != null, "Buffer writer has been set to get to this state."); var data = _bufferWriter.WrittenSpan; GrpcCallLog.SerializedMessage(_call.Logger, _call.RequestType, data.Length); WriteMessage(data); } else { GrpcCallLog.SerializedMessage(_call.Logger, _call.RequestType, _payloadLength.GetValueOrDefault()); } break; default: ThrowInvalidState(_state); break; } }
public override IBufferWriter <byte> GetBufferWriter() { switch (_state) { case InternalState.Initialized: var bufferWriter = ResolveBufferWriter(); // When writing directly to the buffer the header with message size needs to be written first if (DirectSerializationSupported) { CompatibilityHelpers.Assert(_payloadLength != null, "A payload length is required for direct serialization."); EnsureMessageSizeAllowed(_payloadLength.Value); WriteHeader(_buffer, _payloadLength.Value, compress: false); _bufferPosition += GrpcProtocolConstants.HeaderSize; } _state = InternalState.IncompleteBufferWriter; return(bufferWriter); case InternalState.IncompleteBufferWriter: return(ResolveBufferWriter()); default: ThrowInvalidState(_state); return(default !); } }
public override Task ClientStreamWriteAsync(TRequest message) { // The retry client stream writer prevents multiple threads from reaching here. return(DoClientStreamActionAsync(async call => { CompatibilityHelpers.Assert(call.ClientStreamWriter != null); if (ClientStreamWriteOptions != null) { call.ClientStreamWriter.WriteOptions = ClientStreamWriteOptions; } await call.WriteClientStreamAsync(WriteNewMessage, message).ConfigureAwait(false); lock (Lock) { BufferedCurrentMessage = false; } if (ClientStreamComplete) { await call.ClientStreamWriter.CompleteAsync().ConfigureAwait(false); } })); }
private async Task <Metadata> GetResponseHeadersCoreAsync() { CompatibilityHelpers.Assert(_httpResponseTask != null); try { var httpResponse = await _httpResponseTask.ConfigureAwait(false); // Check if the headers have a status. If they do then wait for the overall call task // to complete before returning headers. This means that if the call failed with a // a status then it is possible to await response headers and then call GetStatus(). var grpcStatus = GrpcProtocolHelpers.GetHeaderValue(httpResponse.Headers, GrpcProtocolConstants.StatusTrailer); if (grpcStatus != null) { await CallTask.ConfigureAwait(false); } var metadata = GrpcProtocolHelpers.BuildMetadata(httpResponse.Headers); // https://github.com/grpc/proposal/blob/master/A6-client-retries.md#exposed-retry-metadata if (_attemptCount > 1) { metadata.Add(GrpcProtocolConstants.RetryPreviousAttemptsHeader, (_attemptCount - 1).ToString(CultureInfo.InvariantCulture)); } return(metadata); } catch (Exception ex) when(ResolveException(ErrorStartingCallMessage, ex, out _, out var resolvedException)) { throw resolvedException; } }
public BalancerHttpHandler(HttpMessageHandler innerHandler, HttpHandlerType httpHandlerType, ConnectionManager manager) : base(innerHandler) { _manager = manager; #if NET5_0_OR_GREATER if (httpHandlerType == HttpHandlerType.SocketsHttpHandler) { var socketsHttpHandler = HttpRequestHelpers.GetHttpHandlerType <SocketsHttpHandler>(innerHandler); CompatibilityHelpers.Assert(socketsHttpHandler != null, "Should have handler with this handler type."); socketsHttpHandler.ConnectCallback = OnConnect; } #endif }
private async Task HedgingDelayAsync(TimeSpan hedgingDelay) { CompatibilityHelpers.Assert(_hedgingDelayCts != null); CompatibilityHelpers.Assert(_delayInterruptTcs != null); while (true) { CompatibilityHelpers.Assert(_hedgingDelayCts != null); var completedTask = await Task.WhenAny(Task.Delay(hedgingDelay, _hedgingDelayCts.Token), _delayInterruptTcs.Task).ConfigureAwait(false); if (completedTask != _delayInterruptTcs.Task) { // Task.Delay won. Check CTS to see if it won because of cancellation. _hedgingDelayCts.Token.ThrowIfCancellationRequested(); return; } else { // Cancel the Task.Delay that's no longer needed. // https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/519ef7d231c01116f02bc04354816a735f2a36b6/AsyncGuidance.md#using-a-timeout _hedgingDelayCts.Cancel(); } lock (Lock) { // If we reaching this point then the delay was interrupted. // Need to recreate the delay TCS/CTS for the next cycle. _delayInterruptTcs = new TaskCompletionSource <object?>(TaskCreationOptions.RunContinuationsAsynchronously); _hedgingDelayCts = new CancellationTokenSource(); // Interrupt could come from a pushback, or a failing call with a non-fatal status. if (_pushbackDelay != null) { // Use pushback value and delay again hedgingDelay = _pushbackDelay.GetValueOrDefault(); _pushbackDelay = null; } else { // Immediately return for non-fatal status. return; } } } }
private void SetFailedResult(Status status) { CompatibilityHelpers.Assert(_responseTcs != null); if (Channel.ThrowOperationCanceledOnCancellation && status.StatusCode == StatusCode.DeadlineExceeded) { // Convert status response of DeadlineExceeded to OperationCanceledException when // ThrowOperationCanceledOnCancellation is true. // This avoids a race between the client-side timer and the server status throwing different // errors on deadline exceeded. _responseTcs.TrySetCanceled(); } else { _responseTcs.TrySetException(CreateRpcException(status)); } }
protected bool TryGetTrailers([NotNullWhen(true)] out Metadata?trailers) { if (Trailers == null) { // Trailers are read from the end of the request. // If the request isn't finished then we can't get the trailers. if (!ResponseFinished) { trailers = null; return(false); } CompatibilityHelpers.Assert(HttpResponse != null); Trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.TrailingHeaders()); } trailers = Trailers; return(true); }
private ReadOnlySpan <byte> CompressMessage(ReadOnlySpan <byte> messageData) { CompatibilityHelpers.Assert(_compressionProvider != null, "Compression provider is not null to get here."); GrpcCallLog.CompressingMessage(_call.Logger, _compressionProvider.EncodingName); var output = new MemoryStream(); // Compression stream must be disposed before its content is read. // GZipStream writes final Adler32 at the end of the stream on dispose. using (var compressionStream = _compressionProvider.CreateCompressionStream(output, CompressionLevel.Fastest)) { #if !NETSTANDARD2_0 compressionStream.Write(messageData); #else var array = messageData.ToArray(); compressionStream.Write(array, 0, array.Length); #endif } return(output.GetBuffer().AsSpan(0, (int)output.Length)); }
private ICompressionProvider?ResolveCompressionProvider() { CompatibilityHelpers.Assert( _call.RequestGrpcEncoding != null, "Response encoding should have been calculated at this point."); var canCompress = GrpcProtocolHelpers.CanWriteCompressed(CallOptions.WriteOptions) && !string.Equals(_call.RequestGrpcEncoding, GrpcProtocolConstants.IdentityGrpcEncoding, StringComparison.Ordinal); if (canCompress) { if (_call.Channel.CompressionProviders.TryGetValue(_call.RequestGrpcEncoding, out var compressionProvider)) { return(compressionProvider); } throw new InvalidOperationException($"Could not find compression provider for '{_call.RequestGrpcEncoding}'."); } return(null); }
public override async Task ClientStreamWriteAsync(TRequest message, CancellationToken cancellationToken) { using var registration = (cancellationToken.CanBeCanceled && cancellationToken != Options.CancellationToken) ? RegisterRetryCancellationToken(cancellationToken) : default; // The retry client stream writer prevents multiple threads from reaching here. await DoClientStreamActionAsync(async call => { CompatibilityHelpers.Assert(call.ClientStreamWriter != null); if (ClientStreamWriteOptions != null) { call.ClientStreamWriter.WriteOptions = ClientStreamWriteOptions; } call.TryRegisterCancellation(cancellationToken, out var registration); try { await call.WriteClientStreamAsync(WriteNewMessage, message).ConfigureAwait(false); } finally { registration?.Dispose(); } lock (Lock) { BufferedCurrentMessage = false; } if (ClientStreamComplete) { await call.ClientStreamWriter.CompleteAsync().ConfigureAwait(false); } }).ConfigureAwait(false); }
public async Task AsyncClientStreamingCall_CompressMetadataSentWithRequest_RequestMessageCompressed() { // Arrange HttpRequestMessage?httpRequestMessage = null; HelloRequest? helloRequest1 = null; HelloRequest? helloRequest2 = null; bool?isRequestCompressed1 = null; bool?isRequestCompressed2 = null; var httpClient = ClientTestHelpers.CreateTestClient(async request => { httpRequestMessage = request; var requestData = await request.Content !.ReadAsByteArrayAsync().DefaultTimeout(); var requestStream = new MemoryStream(requestData); isRequestCompressed1 = requestData[0] == 1; helloRequest1 = await StreamSerializationHelper.ReadMessageAsync( requestStream, ClientTestHelpers.ServiceMethod.RequestMarshaller.ContextualDeserializer, "gzip", maximumMessageSize: null, GrpcProtocolConstants.DefaultCompressionProviders, singleMessage: false, CancellationToken.None); isRequestCompressed2 = requestData[requestStream.Position] == 1; helloRequest2 = await StreamSerializationHelper.ReadMessageAsync( requestStream, ClientTestHelpers.ServiceMethod.RequestMarshaller.ContextualDeserializer, "gzip", maximumMessageSize: null, GrpcProtocolConstants.DefaultCompressionProviders, singleMessage: false, CancellationToken.None); var reply = new HelloReply { Message = "Hello world" }; var streamContent = await ClientTestHelpers.CreateResponseContent(reply).DefaultTimeout(); return(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); }); var compressionProviders = GrpcProtocolConstants.DefaultCompressionProviders.Values.ToList(); compressionProviders.Add(new TestCompressionProvider()); var invoker = HttpClientCallInvokerFactory.Create(httpClient, configure: o => o.CompressionProviders = compressionProviders); var compressionMetadata = CreateClientCompressionMetadata("gzip"); var callOptions = new CallOptions(headers: compressionMetadata); // Act var call = invoker.AsyncClientStreamingCall <HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, callOptions); await call.RequestStream.WriteAsync(new HelloRequest { Name = "Hello One" }).DefaultTimeout(); call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); await call.RequestStream.WriteAsync(new HelloRequest { Name = "Hello Two" }).DefaultTimeout(); await call.RequestStream.CompleteAsync().DefaultTimeout(); // Assert var response = await call.ResponseAsync.DefaultTimeout(); Assert.IsNotNull(response); Assert.AreEqual("Hello world", response.Message); CompatibilityHelpers.Assert(httpRequestMessage != null); #if NET6_0_OR_GREATER Assert.AreEqual("identity,gzip,deflate,test", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageAcceptEncodingHeader).Single()); #else Assert.AreEqual("identity,gzip,test", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageAcceptEncodingHeader).Single()); #endif Assert.AreEqual("gzip", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageEncodingHeader).Single()); Assert.AreEqual(false, httpRequestMessage.Headers.Contains(GrpcProtocolConstants.CompressionRequestAlgorithmHeader)); CompatibilityHelpers.Assert(helloRequest1 != null); Assert.AreEqual("Hello One", helloRequest1.Name); CompatibilityHelpers.Assert(helloRequest2 != null); Assert.AreEqual("Hello Two", helloRequest2.Name); Assert.IsTrue(isRequestCompressed1); Assert.IsFalse(isRequestCompressed2); }
public async Task AsyncUnaryCall_CompressMetadataSentWithRequest_RequestMessageCompressed(bool compressionDisabledOnOptions) { // Arrange HttpRequestMessage?httpRequestMessage = null; HelloRequest? helloRequest = null; bool?isRequestNotCompressed = null; var httpClient = ClientTestHelpers.CreateTestClient(async request => { httpRequestMessage = request; var requestData = await request.Content !.ReadAsByteArrayAsync().DefaultTimeout(); isRequestNotCompressed = requestData[0] == 0; helloRequest = await StreamSerializationHelper.ReadMessageAsync( new MemoryStream(requestData), ClientTestHelpers.ServiceMethod.RequestMarshaller.ContextualDeserializer, "gzip", maximumMessageSize: null, GrpcProtocolConstants.DefaultCompressionProviders, singleMessage: true, CancellationToken.None); HelloReply reply = new HelloReply { Message = "Hello world" }; var streamContent = await ClientTestHelpers.CreateResponseContent(reply).DefaultTimeout(); return(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); }); var compressionProviders = GrpcProtocolConstants.DefaultCompressionProviders.Values.ToList(); compressionProviders.Add(new TestCompressionProvider()); var invoker = HttpClientCallInvokerFactory.Create(httpClient, configure: o => o.CompressionProviders = compressionProviders); var compressionMetadata = CreateClientCompressionMetadata("gzip"); var callOptions = new CallOptions(headers: compressionMetadata); if (compressionDisabledOnOptions) { callOptions = callOptions.WithWriteOptions(new WriteOptions(WriteFlags.NoCompress)); } // Act var call = invoker.AsyncUnaryCall <HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, callOptions, new HelloRequest { Name = "Hello" }); // Assert var response = await call.ResponseAsync; Assert.IsNotNull(response); Assert.AreEqual("Hello world", response.Message); CompatibilityHelpers.Assert(httpRequestMessage != null); Assert.AreEqual("identity,gzip,test", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageAcceptEncodingHeader).Single()); Assert.AreEqual("gzip", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageEncodingHeader).Single()); Assert.AreEqual(false, httpRequestMessage.Headers.Contains(GrpcProtocolConstants.CompressionRequestAlgorithmHeader)); CompatibilityHelpers.Assert(helloRequest != null); Assert.AreEqual("Hello", helloRequest.Name); Assert.AreEqual(compressionDisabledOnOptions, isRequestNotCompressed); }
private async Task RunCall(HttpRequestMessage request, TimeSpan?timeout) { using (StartScope()) { var(diagnosticSourceEnabled, activity) = InitializeCall(request, timeout); // Unset variable to check that FinishCall is called in every code path bool finished; Status?status = null; try { // User supplied call credentials could error and so must be run // inside try/catch to handle errors. if (Options.Credentials != null || Channel.CallCredentials?.Count > 0) { await ReadCredentials(request).ConfigureAwait(false); } // Fail early if deadline has already been exceeded _callCts.Token.ThrowIfCancellationRequested(); try { // If a HttpClient has been specified then we need to call it with ResponseHeadersRead // so that the response message is available for streaming var httpResponseTask = (Channel.HttpInvoker is HttpClient httpClient) ? httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _callCts.Token) : Channel.HttpInvoker.SendAsync(request, _callCts.Token); HttpResponse = await httpResponseTask.ConfigureAwait(false); _httpResponseTcs.TrySetResult(HttpResponse); } catch (Exception ex) { if (IsCancellationOrDeadlineException(ex)) { throw; } else { GrpcCallLog.ErrorStartingCall(Logger, ex); throw; } } GrpcCallLog.ResponseHeadersReceived(Logger); status = ValidateHeaders(HttpResponse, out var trailers); if (trailers != null) { Trailers = trailers; } // A status means either the call has failed or grpc-status was returned in the response header if (status != null) { if (_responseTcs != null) { // gRPC status in the header if (status.Value.StatusCode != StatusCode.OK) { finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); } else { // The server should never return StatusCode.OK in the header for a unary call. // If it does then throw an error that no message was returned from the server. GrpcCallLog.MessageNotReturned(Logger); // Change the status code to a more accurate status. // This is consistent with Grpc.Core client behavior. status = new Status(StatusCode.Internal, "Failed to deserialize response message."); finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); } FinishResponseAndCleanUp(status.Value); // Set failed result makes the response task thrown an error. Must be called after // the response is finished. Reasons: // - Finishing the response sets the status. Required for GetStatus to be successful. // - We want GetStatus to always work when called after the response task is done. SetFailedResult(status.Value); } else { finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); FinishResponseAndCleanUp(status.Value); } } else { if (_responseTcs != null) { // Read entire response body immediately and read status from trailers // Trailers are only available once the response body had been read var responseStream = await HttpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); var message = await ReadMessageAsync( responseStream, GrpcProtocolHelpers.GetGrpcEncoding(HttpResponse), singleMessage : true, _callCts.Token).ConfigureAwait(false); status = GrpcProtocolHelpers.GetResponseStatus(HttpResponse, Channel.OperatingSystem.IsBrowser, Channel.HttpHandlerType == HttpHandlerType.WinHttpHandler); if (message == null) { GrpcCallLog.MessageNotReturned(Logger); if (status.Value.StatusCode == StatusCode.OK) { // Change the status code if OK is returned to a more accurate status. // This is consistent with Grpc.Core client behavior. status = new Status(StatusCode.Internal, "Failed to deserialize response message."); } FinishResponseAndCleanUp(status.Value); finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); // Set failed result makes the response task thrown an error. Must be called after // the response is finished. Reasons: // - Finishing the response sets the status. Required for GetStatus to be successful. // - We want GetStatus to always work when called after the response task is done. SetFailedResult(status.Value); } else { GrpcEventSource.Log.MessageReceived(); FinishResponseAndCleanUp(status.Value); finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); if (status.Value.StatusCode == StatusCode.OK) { _responseTcs.TrySetResult(message); } else { // Set failed result makes the response task thrown an error. Must be called after // the response is finished. Reasons: // - Finishing the response sets the status. Required for GetStatus to be successful. // - We want GetStatus to always work when called after the response task is done. SetFailedResult(status.Value); } } } else { // Duplex or server streaming call CompatibilityHelpers.Assert(ClientStreamReader != null); ClientStreamReader.HttpResponseTcs.TrySetResult((HttpResponse, status)); // Wait until the response has been read and status read from trailers. // TCS will also be set in Dispose. status = await CallTask.ConfigureAwait(false); finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); Cleanup(status.Value); } } } catch (Exception ex) { ResolveException(ErrorStartingCallMessage, ex, out status, out var resolvedException); finished = FinishCall(request, diagnosticSourceEnabled, activity, status.Value); // Update HTTP response TCS before clean up. Needs to happen first because cleanup will // cancel the TCS for anyone still listening. _httpResponseTcs.TrySetException(resolvedException); Cleanup(status.Value); // Update response TCS after overall call status is resolved. This is required so that // the call is completed before an error is thrown from ResponseAsync. If it happens // afterwards then there is a chance GetStatus() will error because the call isn't complete. _responseTcs?.TrySetException(resolvedException); } // Verify that FinishCall is called in every code path of this method. // Should create an "Unassigned variable" compiler error if not set. Debug.Assert(finished); } }
private async Task StartRetry(Action <GrpcCall <TRequest, TResponse> > startCallFunc) { Log.StartingRetryWorker(Logger); try { // This is the main retry loop. It will: // 1. Check the result of the active call was successful. // 2. If it was unsuccessful then evaluate if the call can be retried. // 3. If it can be retried then start a new active call and begin again. while (true) { GrpcCall <TRequest, TResponse> currentCall; lock (Lock) { // Start new call. OnStartingAttempt(); currentCall = _activeCall = HttpClientCallInvoker.CreateGrpcCall <TRequest, TResponse>(Channel, Method, Options, AttemptCount); startCallFunc(currentCall); if (CommitedCallTask.IsCompletedSuccessfully()) { // Call has already been commited. This could happen if written messages exceed // buffer limits, which causes the call to immediately become commited and to clear buffers. return; } SetNewActiveCallUnsynchronized(currentCall); } Status?responseStatus; HttpResponseMessage?httpResponse = null; try { currentCall.CancellationToken.ThrowIfCancellationRequested(); CompatibilityHelpers.Assert(currentCall._httpResponseTask != null, "Request should have been made if call is not preemptively cancelled."); httpResponse = await currentCall._httpResponseTask.ConfigureAwait(false); responseStatus = GrpcCall.ValidateHeaders(httpResponse, out _); } catch (Exception ex) { currentCall.ResolveException(GrpcCall <TRequest, TResponse> .ErrorStartingCallMessage, ex, out responseStatus, out _); } CancellationTokenSource.Token.ThrowIfCancellationRequested(); // Check to see the response returned from the server makes the call commited // Null status code indicates the headers were valid and a "Response-Headers" response // was received from the server. // https://github.com/grpc/proposal/blob/master/A6-client-retries.md#when-retries-are-valid if (responseStatus == null) { // Headers were returned. We're commited. CommitCall(currentCall, CommitReason.ResponseHeadersReceived); responseStatus = await currentCall.CallTask.ConfigureAwait(false); if (responseStatus.GetValueOrDefault().StatusCode == StatusCode.OK) { RetryAttemptCallSuccess(); } // Commited so exit retry loop. return; } if (CommitedCallTask.IsCompletedSuccessfully()) { // Call has already been commited. This could happen if written messages exceed // buffer limits, which causes the call to immediately become commited and to clear buffers. return; } Status status = responseStatus.Value; var retryPushbackMS = GetRetryPushback(httpResponse); // Failures only count towards retry throttling if they have a known, retriable status. // This stops non-transient statuses, e.g. INVALID_ARGUMENT, from triggering throttling. if (_retryPolicy.RetryableStatusCodes.Contains(status.StatusCode) || retryPushbackMS < 0) { RetryAttemptCallFailure(); } var result = EvaluateRetry(status, retryPushbackMS); Log.RetryEvaluated(Logger, status.StatusCode, AttemptCount, result == null); if (result == null) { TimeSpan delayDuration; if (retryPushbackMS != null) { delayDuration = TimeSpan.FromMilliseconds(retryPushbackMS.GetValueOrDefault()); _nextRetryDelayMilliseconds = retryPushbackMS.GetValueOrDefault(); } else { delayDuration = TimeSpan.FromMilliseconds(Channel.GetRandomNumber(0, Convert.ToInt32(_nextRetryDelayMilliseconds))); } Log.StartingRetryDelay(Logger, delayDuration); await Task.Delay(delayDuration, CancellationTokenSource.Token).ConfigureAwait(false); _nextRetryDelayMilliseconds = CalculateNextRetryDelay(); // Check if dispose was called on call. CancellationTokenSource.Token.ThrowIfCancellationRequested(); // Clean up the failed call. currentCall.Dispose(); } else { // Handle the situation where the call failed with a non-deadline status, but retry // didn't happen because of deadline exceeded. IGrpcCall <TRequest, TResponse> resolvedCall = (IsDeadlineExceeded() && !(currentCall.CallTask.IsCompletedSuccessfully() && currentCall.CallTask.Result.StatusCode == StatusCode.DeadlineExceeded)) ? CreateStatusCall(GrpcProtocolConstants.DeadlineExceededStatus) : currentCall; // Can't retry. // Signal public API exceptions that they should finish throwing and then exit the retry loop. CommitCall(resolvedCall, result.GetValueOrDefault()); return; } } } catch (Exception ex) { HandleUnexpectedError(ex); } finally { Log.StoppingRetryWorker(Logger); } }
public Task <TResponse> GetResponseAsync() { CompatibilityHelpers.Assert(_responseTcs != null); return(_responseTcs.Task); }
private async Task StartCall(Action <GrpcCall <TRequest, TResponse> > startCallFunc) { GrpcCall <TRequest, TResponse> call; lock (Lock) { if (CommitedCallTask.IsCompletedSuccessfully()) { // Call has already been commited. This could happen if written messages exceed // buffer limits, which causes the call to immediately become commited and to clear buffers. return; } OnStartingAttempt(); call = HttpClientCallInvoker.CreateGrpcCall <TRequest, TResponse>(Channel, Method, Options, AttemptCount); _activeCalls.Add(call); startCallFunc(call); SetNewActiveCallUnsynchronized(call); } Status?responseStatus; HttpResponseMessage?httpResponse = null; try { call.CancellationToken.ThrowIfCancellationRequested(); CompatibilityHelpers.Assert(call._httpResponseTask != null, "Request should have been made if call is not preemptively cancelled."); httpResponse = await call._httpResponseTask.ConfigureAwait(false); responseStatus = GrpcCall.ValidateHeaders(httpResponse, out _); } catch (Exception ex) { call.ResolveException(GrpcCall <TRequest, TResponse> .ErrorStartingCallMessage, ex, out responseStatus, out _); } if (CancellationTokenSource.IsCancellationRequested) { CommitCall(call, CommitReason.Canceled); return; } // Check to see the response returned from the server makes the call commited // Null status code indicates the headers were valid and a "Response-Headers" response // was received from the server. // https://github.com/grpc/proposal/blob/master/A6-client-retries.md#when-retries-are-valid if (responseStatus == null) { // Headers were returned. We're commited. CommitCall(call, CommitReason.ResponseHeadersReceived); // Wait until the call has finished and then check its status code // to update retry throttling tokens. var status = await call.CallTask.ConfigureAwait(false); if (status.StatusCode == StatusCode.OK) { RetryAttemptCallSuccess(); } } else { var status = responseStatus.Value; var retryPushbackMS = GetRetryPushback(httpResponse); if (retryPushbackMS < 0) { RetryAttemptCallFailure(); } else if (_hedgingPolicy.NonFatalStatusCodes.Contains(status.StatusCode)) { // Needs to happen before interrupt. RetryAttemptCallFailure(); // No need to interrupt if we started with no delay and all calls // have already been made when hedging starting. if (_delayInterruptTcs != null) { lock (Lock) { if (retryPushbackMS >= 0) { _pushbackDelay = TimeSpan.FromMilliseconds(retryPushbackMS.GetValueOrDefault()); } _delayInterruptTcs.TrySetResult(null); } } } else { CommitCall(call, CommitReason.FatalStatusCode); } } lock (Lock) { if (IsDeadlineExceeded()) { // Deadline has been exceeded so immediately commit call. CommitCall(call, CommitReason.DeadlineExceeded); } else if (_activeCalls.Count == 1 && AttemptCount >= MaxRetryAttempts) { // This is the last active call and no more will be made. CommitCall(call, CommitReason.ExceededAttemptCount); } else if (_activeCalls.Count == 1 && IsRetryThrottlingActive()) { // This is the last active call and throttling is active. CommitCall(call, CommitReason.Throttled); } else { // Call isn't used and can be cancelled. // Note that the call could have already been removed and disposed if the // hedging call has been finalized or disposed. if (_activeCalls.Remove(call)) { call.Dispose(); } } } }