< 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: 275_13775359185
Line coverage
100%
Covered lines: 60
Uncovered lines: 0
Coverable lines: 60
Total lines: 127
Line coverage: 100%
Branch coverage
100%
Covered branches: 14
Total branches: 14
Branch coverage: 100%
Method coverage
100%
Covered methods: 12
Total methods: 12
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;
 137714    private TimeSpan _idleTimeout = Timeout.InfiniteTimeSpan;
 137715    private readonly CancellationTokenSource _readCts = new();
 16
 17    private readonly Timer? _readTimer;
 18    private readonly Timer? _writeTimer;
 19
 20    public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken) =>
 133321        _decoratee.ConnectAsync(cancellationToken);
 22
 23    public void Dispose()
 137524    {
 137525        _decoratee.Dispose();
 137526        _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.
 137529        _readTimer?.Dispose();
 137530        _writeTimer?.Dispose();
 137531    }
 32
 33    public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 9410834    {
 9410835        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 9410836            _decoratee.ReadAsync(buffer, cancellationToken) :
 9410837            PerformReadAsync();
 38
 39        async ValueTask<int> PerformReadAsync()
 9280940        {
 41            try
 9280942            {
 9280943                using CancellationTokenRegistration _ = cancellationToken.UnsafeRegister(
 50844                    cts => ((CancellationTokenSource)cts!).Cancel(),
 9280945                    _readCts);
 9280946                _readCts.CancelAfter(_idleTimeout); // enable idle timeout before reading
 47
 9280948                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.
 9188751                if (bytesRead > 0)
 9163152                {
 9163153                    ResetReadTimer();
 9163154                }
 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
 9188758                return bytesRead;
 59            }
 47060            catch (OperationCanceledException)
 47061            {
 47062                cancellationToken.ThrowIfCancellationRequested();
 63
 264                throw new IceRpcException(
 265                    IceRpcError.ConnectionIdle,
 266                    $"The connection did not receive any bytes for over {_idleTimeout.TotalSeconds} s.");
 67            }
 68            finally
 9280969            {
 9280970                _readCts.CancelAfter(Timeout.InfiniteTimeSpan); // disable idle timeout if not canceled
 9280971            }
 9188772        }
 9410873    }
 74
 75    public Task ShutdownWriteAsync(CancellationToken cancellationToken) =>
 27076        _decoratee.ShutdownWriteAsync(cancellationToken);
 77
 78    public ValueTask WriteAsync(ReadOnlySequence<byte> buffer, CancellationToken cancellationToken)
 764779    {
 764780        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 764781            _decoratee.WriteAsync(buffer, cancellationToken) :
 764782            PerformWriteAsync();
 83
 84        async ValueTask PerformWriteAsync()
 699685        {
 699686            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.
 698190            ResetWriteTimer();
 698191        }
 764792    }
 93
 94    /// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>.
 95    /// </summary>
 275496    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(IDuplexConnection decoratee, Action sendReadPing, Action sendWritePing)
 698101        : this(decoratee)
 698102    {
 727103        _readTimer = new Timer(_ => sendReadPing());
 700104        _writeTimer = new Timer(_ => sendWritePing());
 698105    }
 106
 107    /// <summary>Sets the idle timeout and schedules pings once the connection is established.</summary>.
 108    internal void Enable(TimeSpan idleTimeout)
 1224109    {
 1224110        Debug.Assert(idleTimeout != Timeout.InfiniteTimeSpan);
 1224111        _idleTimeout = idleTimeout;
 112
 1224113        ResetReadTimer();
 1224114        ResetWriteTimer();
 1224115    }
 116
 117    /// <summary>Resets the read timer. We send a "read" ping when this timer expires.</summary>
 118    /// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks>
 92855119    private void ResetReadTimer() => _readTimer?.Change(_idleTimeout * 0.5, Timeout.InfiniteTimeSpan);
 120
 121    /// <summary>Resets the write timer. We send a "write" ping when this timer expires.</summary>
 122    /// <remarks>This method is no-op unless this decorator is constructed with send ping actions.</remarks>
 123    // The write timer factor (0.6) was chosen to be greater than the read timer factor (0.5). This way, when the
 124    // connection is completely idle, the read timer expires before the write timer and has time to send a ping that
 125    // resets the write timer. This reduces the likelihood of duplicate "keep alive" pings.
 8205126    private void ResetWriteTimer() => _writeTimer?.Change(_idleTimeout * 0.6, Timeout.InfiniteTimeSpan);
 127}