< Summary

Information
Class: IceRpc.Retry.RetryInterceptor
Assembly: IceRpc.Retry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Retry/RetryInterceptor.cs
Tag: 275_13775359185
Line coverage
94%
Covered lines: 83
Uncovered lines: 5
Coverable lines: 88
Total lines: 195
Line coverage: 94.3%
Branch coverage
84%
Covered branches: 42
Total branches: 50
Branch coverage: 84%
Method coverage
100%
Covered methods: 4
Total methods: 4
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()82.6%46.044697.33%
RethrowException(...)100%1.22140%
CreateRetryLogScope(...)100%44100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Retry/RetryInterceptor.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Extensions.DependencyInjection;
 4using IceRpc.Features;
 5using IceRpc.Retry.Internal;
 6using Microsoft.Extensions.Logging;
 7using Microsoft.Extensions.Logging.Abstractions;
 8using System.Diagnostics;
 9using System.Runtime.ExceptionServices;
 10
 11namespace IceRpc.Retry;
 12
 13/// <summary>The retry interceptor is responsible for retrying failed requests when the failure condition can be
 14/// retried.</summary>
 15/// <remarks>A failed request can be retried if:
 16/// <list type="bullet">
 17/// <item><description><see cref="RetryOptions.MaxAttempts" /> is not reached.</description></item>
 18/// <item><description><see cref="OutgoingFrame.Payload" /> can be read again.</description></item>
 19/// <item><description>The failure condition can be retried.</description></item>
 20/// </list>
 21/// <para>In order to be able to read again the request's payload, the retry interceptor decorates the payload
 22/// with <see cref="ResettablePipeReaderDecorator" />. The decorator can be reset as long as the buffered data doesn't
 23/// exceed <see cref="RetryOptions.MaxPayloadSize" />.</para>
 24/// <para>The request can be retried under the following failure conditions:</para>
 25/// <list type="bullet">
 26/// <item><description>The status code carried by the response is <see cref="StatusCode.Unavailable"
 27/// />.</description></item>
 28/// <item><description>The status code carried by the response is <see cref="StatusCode.NotFound" /> and the
 29/// protocol is ice.</description></item>
 30/// <item><description>The request failed with an <see cref="IceRpcException" /> with one of the following error:
 31/// <list type="bullet">
 32/// <item><description>The error code is <see cref="IceRpcError.InvocationCanceled" />.</description></item>
 33/// <item><description>The error code is <see cref="IceRpcError.ConnectionAborted" /> or <see
 34/// cref="IceRpcError.TruncatedData" /> and the request has the <see cref="RequestFieldKey.Idempotent" />
 35/// field.</description></item>
 36/// </list></description></item>
 37/// </list>
 38/// <para>If the status code carried by the response is <see cref="StatusCode.Unavailable" /> or <see
 39/// cref="StatusCode.NotFound" /> (with the ice protocol), the address of the server is removed from the set of
 40/// server addresses to retry on. This ensures the request won't be retried on the unavailable server.</para></remarks>
 41/// <seealso cref="RetryPipelineExtensions"/>
 42/// <seealso cref="RetryInvokerBuilderExtensions"/>
 43public class RetryInterceptor : IInvoker
 44{
 45    private readonly ILogger _logger;
 46    private readonly int _maxAttempts;
 47    private readonly int _maxPayloadSize;
 48    private readonly IInvoker _next;
 49
 50    /// <summary>Constructs a retry interceptor.</summary>
 51    /// <param name="next">The next invoker in the invocation pipeline.</param>
 52    /// <param name="options">The options to configure the retry interceptor.</param>
 53    /// <param name="logger">The logger.</param>
 1654    public RetryInterceptor(IInvoker next, RetryOptions options, ILogger logger)
 1655    {
 1656        _next = next;
 1657        _maxAttempts = options.MaxAttempts;
 1658        _maxPayloadSize = options.MaxPayloadSize;
 1659        _logger = logger;
 1660    }
 61
 62    /// <inheritdoc/>
 63    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 1664    {
 65        // This interceptor does not support retrying requests with a payload continuation.
 1666        if (request.PayloadContinuation is not null || _maxAttempts == 1)
 067        {
 068            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 69        }
 70        else
 1671        {
 1672            var decorator = new ResettablePipeReaderDecorator(request.Payload, _maxPayloadSize);
 1673            request.Payload = decorator;
 74
 75            try
 1676            {
 1677                int attempt = 1;
 1678                IncomingResponse? response = null;
 1679                IceRpcException? exception = null;
 80                bool tryAgain;
 81
 82                do
 2683                {
 2684                    bool retryWithOtherReplica = false;
 85
 86                    try
 2687                    {
 2688                        using IDisposable? scope = CreateRetryLogScope(attempt);
 89
 2690                        response = await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 91
 1592                        if (response.StatusCode == StatusCode.Unavailable ||
 1593                            (response.Protocol == Protocol.Ice && response.StatusCode == StatusCode.NotFound))
 494                        {
 495                            retryWithOtherReplica = true;
 496                        }
 97                        else
 1198                        {
 1199                            return response;
 100                        }
 4101                    }
 9102                    catch (IceRpcException iceRpcException) when (
 9103                        iceRpcException.IceRpcError == IceRpcError.NoConnection)
 1104                    {
 105                        // NoConnection is always considered non-retryable; it typically occurs because we removed all
 106                        // the server addresses from serverAddressFeature. Unlike other non-retryable exceptions, we
 107                        // privilege returning the previous response (if any).
 1108                        return response ?? throw RethrowException(exception ?? iceRpcException);
 109                    }
 8110                    catch (IceRpcException iceRpcException)
 8111                    {
 8112                        response = null;
 8113                        exception = iceRpcException;
 8114                    }
 115
 12116                    Debug.Assert(retryWithOtherReplica || exception is not null);
 117
 118                    // Check if we can retry
 12119                    tryAgain = false;
 120
 121                    // The decorator is non-resettable when we've reached the last attempt (see below) or the decorator
 122                    // caught an exception that prevents resetting, like a ReadAsync exception.
 12123                    if (decorator.IsResettable)
 11124                    {
 11125                        if (retryWithOtherReplica)
 4126                        {
 4127                            if (request.Features.Get<IServerAddressFeature>() is IServerAddressFeature serverAddressFeat
 4128                                serverAddressFeature.ServerAddress is ServerAddress mainServerAddress)
 4129                            {
 130                                // We don't want to retry with this server address
 4131                                serverAddressFeature.RemoveServerAddress(mainServerAddress);
 132
 4133                                tryAgain = serverAddressFeature.ServerAddress is not null;
 4134                            }
 135                            // else there is no replica to retry with
 4136                        }
 137                        else
 7138                        {
 7139                            Debug.Assert(exception is not null);
 140                            // It's always safe to retry InvocationCanceled because it's only raised before the request
 141                            // is sent to the peer. For idempotent requests we also retry on ConnectionAborted and
 142                            // TruncatedData.
 7143                            tryAgain = exception.IceRpcError switch
 7144                            {
 5145                                IceRpcError.InvocationCanceled => true,
 7146                                IceRpcError.ConnectionAborted or IceRpcError.TruncatedData
 2147                                    when request.Fields.ContainsKey(RequestFieldKey.Idempotent) => true,
 1148                                _ => false
 7149                            };
 7150                        }
 151
 11152                        if (tryAgain)
 10153                        {
 10154                            attempt++;
 10155                            decorator.Reset();
 156
 157                            // If this attempt is the last attempt, we make the decorator non-resettable to release the
 158                            // memory for the request payload as soon as possible.
 10159                            if (attempt == _maxAttempts)
 5160                            {
 5161                               decorator.IsResettable = false;
 5162                            }
 10163                        }
 11164                    }
 12165                }
 12166                while (tryAgain);
 167
 2168                Debug.Assert(response is not null || exception is not null);
 2169                Debug.Assert(response is null || response.StatusCode != StatusCode.Ok);
 2170                return response ?? throw RethrowException(exception!);
 171            }
 172            finally
 16173            {
 174                // We want to leave request.Payload in a correct, usable state when we exit. Usually request.Payload
 175                // will get completed by the caller, and we want this Complete call to flow through to the decoratee. If
 176                // the payload is still readable (e.g. we received a non-retryable exception before reading anything or
 177                // just after a Reset), an upstream interceptor may want to attempt another call that reads this payload
 178                // and the now non-resettable decorator will provide the correct behavior. The decorator ensures that
 179                // calls to AdvanceTo on the decoratee always receive ever-increasing examined values even after one or
 180                // more Resets.
 16181                decorator.IsResettable = false;
 16182            }
 183        }
 11184    }
 185
 186    private static Exception RethrowException(Exception exception)
 3187    {
 3188        ExceptionDispatchInfo.Throw(exception);
 0189        Debug.Assert(false);
 0190        return exception;
 0191    }
 192
 193    private IDisposable? CreateRetryLogScope(int attempt) =>
 26194        _logger != NullLogger.Instance && attempt > 1 ? _logger.RetryScope(attempt, _maxAttempts) : null;
 195}