< 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: 1856_27024993493
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;
 75514    private TimeSpan _idleTimeout = Timeout.InfiniteTimeSpan;
 75515    private readonly CancellationTokenSource _readCts = new();
 16
 17    private readonly Timer? _readTimer;
 18    private readonly Timer? _writeTimer;
 19
 20    public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken) =>
 73321        _decoratee.ConnectAsync(cancellationToken);
 22
 23    public void Dispose()
 75424    {
 75425        _decoratee.Dispose();
 75426        _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.
 75429        _readTimer?.Dispose();
 75430        _writeTimer?.Dispose();
 75431    }
 32
 33    public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 2936834    {
 2936835        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 2936836            _decoratee.ReadAsync(buffer, cancellationToken) :
 2936837            PerformReadAsync();
 38
 39        async ValueTask<int> PerformReadAsync()
 2865340        {
 41            try
 2865342            {
 2865343                using CancellationTokenRegistration _ = cancellationToken.UnsafeRegister(
 30444                    cts => ((CancellationTokenSource)cts!).Cancel(),
 2865345                    _readCts);
 2865346                _readCts.CancelAfter(_idleTimeout); // enable idle timeout before reading
 47
 2865348                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.
 2814451                if (bytesRead > 0)
 2800852                {
 2800853                    ResetReadTimer();
 2800854                }
 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
 2814458                return bytesRead;
 59            }
 25460            catch (OperationCanceledException)
 25461            {
 25462                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
 2865369            {
 2865370                _readCts.CancelAfter(Timeout.InfiniteTimeSpan); // disable idle timeout if not canceled
 2865371            }
 2814472        }
 2936873    }
 74
 75    public Task ShutdownWriteAsync(CancellationToken cancellationToken) =>
 14376        _decoratee.ShutdownWriteAsync(cancellationToken);
 77
 78    public ValueTask WriteAsync(ReadOnlySequence<byte> buffer, CancellationToken cancellationToken)
 648379    {
 648380        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 648381            _decoratee.WriteAsync(buffer, cancellationToken) :
 648382            PerformWriteAsync();
 83
 84        async ValueTask PerformWriteAsync()
 612685        {
 612686            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.
 612490            ResetWriteTimer();
 612491        }
 648392    }
 93
 94    /// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>.
 95    /// </summary>
 151096    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)
 379104        : this(decoratee)
 379105    {
 394106        _readTimer = new Timer(_ => sendReadPing());
 380107        _writeTimer = new Timer(_ => sendWritePing());
 379108    }
 109
 110    /// <summary>Sets the idle timeout and schedules pings once the connection is established.</summary>.
 111    internal void Enable(TimeSpan idleTimeout)
 676112    {
 676113        Debug.Assert(idleTimeout != Timeout.InfiniteTimeSpan);
 676114        _idleTimeout = idleTimeout;
 115
 676116        ResetReadTimer();
 676117        ResetWriteTimer();
 676118    }
 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>
 28684122    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.
 6800129    private void ResetWriteTimer() => _writeTimer?.Change(_idleTimeout * 0.6, Timeout.InfiniteTimeSpan);
 130}