< Summary

Information
Class: IceRpc.Transports.Slic.Internal.SlicDuplexConnectionDecorator
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Transports/Slic/Internal/SlicDuplexConnectionDecorator.cs
Tag: 2300_35243572715
Line coverage
100%
Covered lines: 60
Uncovered lines: 0
Coverable lines: 60
Total lines: 130
Line coverage: 100%
Branch coverage
100%
Covered branches: 14
Total branches: 14
Branch coverage: 100%
Method coverage
100%
Covered methods: 12
Fully covered methods: 12
Total methods: 12
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ConnectAsync(...)100%11100%
Dispose()100%44100%
ReadAsync(...)100%22100%
PerformReadAsync()100%22100%
ShutdownWriteAsync(...)100%11100%
WriteAsync(...)100%22100%
PerformWriteAsync()100%11100%
.ctor(...)100%11100%
Enable(...)100%11100%
ResetReadTimer()100%22100%
ResetWriteTimer()100%22100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Transports/Slic/Internal/SlicDuplexConnectionDecorator.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using System.Buffers;
 4using System.Diagnostics;
 5
 6namespace IceRpc.Transports.Slic.Internal;
 7
 8/// <summary>Decorates <see cref="ReadAsync" /> to fail if no byte is received for over idle timeout. Also optionally
 9/// decorates both <see cref="ReadAsync"/> and <see cref="WriteAsync" /> to schedule pings that prevent both the local
 10/// and remote idle timers from expiring.</summary>
 11internal class SlicDuplexConnectionDecorator : IDuplexConnection
 12{
 13    private readonly IDuplexConnection _decoratee;
 80114    private TimeSpan _idleTimeout = Timeout.InfiniteTimeSpan;
 80115    private readonly CancellationTokenSource _readCts = new();
 16
 17    private readonly Timer? _readTimer;
 18    private readonly Timer? _writeTimer;
 19
 20    public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken) =>
 77921        _decoratee.ConnectAsync(cancellationToken);
 22
 23    public void Dispose()
 80024    {
 80025        _decoratee.Dispose();
 80026        _readCts.Dispose();
 27
 28        // Using Dispose is fine, there's no need to wait for the keep alive action to terminate if it's running.
 80029        _readTimer?.Dispose();
 80030        _writeTimer?.Dispose();
 80031    }
 32
 33    public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 2971234    {
 2971235        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 2971236            _decoratee.ReadAsync(buffer, cancellationToken) :
 2971237            PerformReadAsync();
 38
 39        async ValueTask<int> PerformReadAsync()
 2895040        {
 41            try
 2895042            {
 2895043                using CancellationTokenRegistration _ = cancellationToken.UnsafeRegister(
 30944                    cts => ((CancellationTokenSource)cts!).Cancel(),
 2895045                    _readCts);
 2895046                _readCts.CancelAfter(_idleTimeout); // enable idle timeout before reading
 47
 2895048                int bytesRead = await _decoratee.ReadAsync(buffer, _readCts.Token).ConfigureAwait(false);
 49
 50                // After each successful read, we schedule one ping some time in the future.
 2839551                if (bytesRead > 0)
 2825852                {
 2825853                    ResetReadTimer();
 2825854                }
 55                // When 0, the other side called ShutdownWriteAsync, so there is no point to send a ping since we can't
 56                // get back a pong.
 57
 2839558                return bytesRead;
 59            }
 28260            catch (OperationCanceledException)
 28261            {
 28262                cancellationToken.ThrowIfCancellationRequested();
 63
 164                throw new IceRpcException(
 165                    IceRpcError.ConnectionIdle,
 166                    $"The connection did not receive any bytes for over {_idleTimeout.TotalSeconds} s.");
 67            }
 68            finally
 2895069            {
 2895070                _readCts.CancelAfter(Timeout.InfiniteTimeSpan); // disable idle timeout if not canceled
 2895071            }
 2839572        }
 2971273    }
 74
 75    public Task ShutdownWriteAsync(CancellationToken cancellationToken) =>
 14476        _decoratee.ShutdownWriteAsync(cancellationToken);
 77
 78    public ValueTask WriteAsync(ReadOnlySequence<byte> buffer, CancellationToken cancellationToken)
 608679    {
 608680        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 608681            _decoratee.WriteAsync(buffer, cancellationToken) :
 608682            PerformWriteAsync();
 83
 84        async ValueTask PerformWriteAsync()
 571485        {
 571486            await _decoratee.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
 87
 88            // After each successful write, we schedule one ping some time in the future. Since each ping is itself a
 89            // write, if there is no application activity at all, we'll send successive pings at regular intervals.
 570890            ResetWriteTimer();
 570891        }
 608692    }
 93
 94    /// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>.
 95    /// </summary>
 160296    internal SlicDuplexConnectionDecorator(IDuplexConnection decoratee) => _decoratee = decoratee;
 97
 98    /// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>.
 99    /// </summary>
 100    internal SlicDuplexConnectionDecorator(
 101        IDuplexConnection decoratee,
 102        Action sendReadPing,
 103        Action sendWritePing)
 398104        : this(decoratee)
 398105    {
 413106        _readTimer = new Timer(_ => sendReadPing());
 399107        _writeTimer = new Timer(_ => sendWritePing());
 398108    }
 109
 110    /// <summary>Sets the idle timeout and schedules pings once the connection is established.</summary>.
 111    internal void Enable(TimeSpan idleTimeout)
 721112    {
 721113        Debug.Assert(idleTimeout != Timeout.InfiniteTimeSpan);
 721114        _idleTimeout = idleTimeout;
 115
 721116        ResetReadTimer();
 721117        ResetWriteTimer();
 721118    }
 119
 120    /// <summary>Resets the read timer. We send a "read" ping when this timer expires.</summary>
 121    /// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks>
 28979122    private void ResetReadTimer() => _readTimer?.Change(_idleTimeout * 0.5, Timeout.InfiniteTimeSpan);
 123
 124    /// <summary>Resets the write timer. We send a "write" ping when this timer expires.</summary>
 125    /// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks>
 126    // The write timer factor (0.6) was chosen to be greater than the read timer factor (0.5). This way, when the
 127    // connection is completely idle, the read timer expires before the write timer and has time to send a ping that
 128    // resets the write timer. This reduces the likelihood of duplicate "keep alive" pings.
 6429129    private void ResetWriteTimer() => _writeTimer?.Change(_idleTimeout * 0.6, Timeout.InfiniteTimeSpan);
 130}