< Summary

Information
Class: IceRpc.Deadline.DeadlineInterceptor
Assembly: IceRpc.Deadline
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Deadline/DeadlineInterceptor.cs
Tag: 1856_27024993493
Line coverage
96%
Covered lines: 56
Uncovered lines: 2
Coverable lines: 58
Total lines: 130
Line coverage: 96.5%
Branch coverage
83%
Covered branches: 25
Total branches: 30
Branch coverage: 83.3%
Method coverage
100%
Covered methods: 4
Fully covered methods: 3
Total methods: 4
Method coverage: 100%
Full method coverage: 75%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%88100%
InvokeAsync(...)75%202093.33%
PerformInvokeAsync()100%22100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Deadline/DeadlineInterceptor.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Extensions.DependencyInjection;
 4using IceRpc.Features;
 5using ZeroC.Slice.Codec;
 6
 7namespace IceRpc.Deadline;
 8
 9/// <summary>Represents an interceptor that sets deadlines on requests without deadlines, and enforces these deadlines.
 10/// </summary>
 11/// <remarks>When a request doesn't carry an <see cref="IDeadlineFeature"/> feature, this interceptor computes a
 12/// deadline using its configured default timeout; otherwise, it uses the request's existing deadline feature. It then
 13/// encodes the deadline value as a <see cref="RequestFieldKey.Deadline" /> field and makes the invocation throw a
 14/// <see cref="TimeoutException" /> upon expiration of this deadline.<br/>
 15/// The dispatch of a one-way request cannot be canceled since the invocation typically completes before this dispatch
 16/// starts; as a result, for a one-way request, the deadline must be enforced by a <see cref="DeadlineMiddleware"/>.
 17/// <br/>
 18/// If the server installs a <see cref="DeadlineMiddleware"/>, this deadline middleware decodes the deadline and
 19/// enforces it. In the unlikely event the middleware detects the expiration of the deadline before this interceptor,
 20/// the invocation will return an <see cref="OutgoingResponse"/> carrying status code
 21/// <see cref="StatusCode.DeadlineExceeded"/>.<br/>
 22/// The deadline interceptor must be installed before any interceptor than can run multiple times per request. In
 23/// particular, it must be installed before the retry interceptor.</remarks>
 24/// <seealso cref="DeadlinePipelineExtensions"/>
 25/// <seealso cref="DeadlineInvokerBuilderExtensions"/>
 26public class DeadlineInterceptor : IInvoker
 27{
 28    // The maximum supported timeout (int.MaxValue ms, ~24.8 days). This is the maximum delay
 29    // CancellationTokenSource.CancelAfter accepts.
 130    private static readonly TimeSpan _maxSupportedTimeout = TimeSpan.FromMilliseconds(int.MaxValue);
 31
 32    private readonly bool _alwaysEnforceDeadline;
 33    private readonly IInvoker _next;
 34    private readonly TimeSpan _defaultTimeout;
 35    private readonly TimeProvider _timeProvider;
 36
 37    /// <summary>Constructs a Deadline interceptor.</summary>
 38    /// <param name="next">The next invoker in the invocation pipeline.</param>
 39    /// <param name="defaultTimeout">The default timeout applied to requests without a deadline. Must be a positive
 40    /// value not exceeding ~24.8 days, or <see cref="Timeout.InfiniteTimeSpan" /> to disable the default timeout
 41    /// entirely.</param>
 42    /// <param name="timeProvider">The optional time provider used to obtain the current time. If
 43    /// <see langword="null"/>, it uses <see cref="TimeProvider.System"/>.</param>
 44    /// <param name="alwaysEnforceDeadline">When <see langword="true" /> and the request carries a deadline, the
 45    /// interceptor always creates a cancellation token source to enforce this deadline. When <see langword="false" />
 46    /// and the request carries a deadline, the interceptor creates a cancellation token source to enforce this deadline
 47    /// only when the invocation's cancellation token cannot be canceled. The default value is <see langword="false" />.
 48    /// </param>
 49    /// <exception cref="ArgumentException">Thrown if <paramref name="defaultTimeout" /> is neither
 50    /// <see cref="Timeout.InfiniteTimeSpan" /> nor a positive value within the supported range.</exception>
 1351    public DeadlineInterceptor(IInvoker next, TimeSpan defaultTimeout, bool alwaysEnforceDeadline, TimeProvider? timePro
 1352    {
 1353        if (defaultTimeout != Timeout.InfiniteTimeSpan &&
 1354            (defaultTimeout <= TimeSpan.Zero || defaultTimeout > _maxSupportedTimeout))
 355        {
 356            throw new ArgumentException(
 357                $"The {nameof(defaultTimeout)} value must be Timeout.InfiniteTimeSpan or a positive value not exceeding 
 358                nameof(defaultTimeout));
 59        }
 60
 1061        _next = next;
 1062        _alwaysEnforceDeadline = alwaysEnforceDeadline;
 1063        _defaultTimeout = defaultTimeout;
 1064        _timeProvider = timeProvider ?? TimeProvider.System;
 1065    }
 66
 67    /// <inheritdoc/>
 68    public Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken = default)
 969    {
 970        TimeSpan? timeout = null;
 971        DateTime deadline = DateTime.MaxValue;
 72
 973        DateTime now = _timeProvider.GetUtcNow().UtcDateTime;
 974        if (request.Features.Get<IDeadlineFeature>() is IDeadlineFeature deadlineFeature)
 775        {
 76            // Normalize to UTC.
 777            deadline = deadlineFeature.Value == DateTime.MaxValue ?
 778                DateTime.MaxValue : deadlineFeature.Value.ToUniversalTime();
 779            if (deadline != DateTime.MaxValue && (_alwaysEnforceDeadline || !cancellationToken.CanBeCanceled))
 680            {
 681                timeout = deadline - now;
 682            }
 783        }
 284        else if (_defaultTimeout != Timeout.InfiniteTimeSpan)
 285        {
 286            timeout = _defaultTimeout;
 287            deadline = now + timeout.Value;
 288        }
 89
 990        if (timeout is not null && timeout.Value <= TimeSpan.Zero)
 091        {
 092            throw new TimeoutException("The request deadline has expired.");
 93        }
 94
 995        if (deadline != DateTime.MaxValue)
 996        {
 997            request.Fields = request.Fields.With(
 998                RequestFieldKey.Deadline,
 999                deadline,
 14100                (ref SliceEncoder encoder, DateTime deadline) => encoder.EncodeTimeStamp(deadline));
 9101        }
 102
 9103        return timeout is null ? _next.InvokeAsync(request, cancellationToken) : PerformInvokeAsync(timeout.Value);
 104
 105        async Task<IncomingResponse> PerformInvokeAsync(TimeSpan timeout)
 8106        {
 107            // Reject a caller-provided IDeadlineFeature whose remaining timeout exceeds what this interceptor
 108            // can enforce. Such deadlines are nonsensical in practice (~24.8 days), and silently clamping the
 109            // value the caller asked for is worse than failing cleanly.
 8110            if (timeout > _maxSupportedTimeout)
 1111            {
 1112                throw new NotSupportedException(
 1113                    $"The request deadline exceeds the maximum timeout supported by this interceptor: {_maxSupportedTime
 114            }
 115
 7116            using var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 7117            timeoutTokenSource.CancelAfter(timeout);
 118
 119            try
 7120            {
 7121                return await _next.InvokeAsync(request, timeoutTokenSource.Token).ConfigureAwait(false);
 122            }
 2123            catch (OperationCanceledException exception) when (exception.CancellationToken == timeoutTokenSource.Token)
 2124            {
 2125                cancellationToken.ThrowIfCancellationRequested();
 2126                throw new TimeoutException("The request deadline has expired.");
 127            }
 5128        }
 9129    }
 130}