< 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: 701_22528036593
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;
 70514    private TimeSpan _idleTimeout = Timeout.InfiniteTimeSpan;
 70515    private readonly CancellationTokenSource _readCts = new();
 16
 17    private readonly Timer? _readTimer;
 18    private readonly Timer? _writeTimer;
 19
 20    public Task<TransportConnectionInformation> ConnectAsync(CancellationToken cancellationToken) =>
 68321        _decoratee.ConnectAsync(cancellationToken);
 22
 23    public void Dispose()
 70324    {
 70325        _decoratee.Dispose();
 70326        _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.
 70329        _readTimer?.Dispose();
 70330        _writeTimer?.Dispose();
 70331    }
 32
 33    public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
 4948834    {
 4948835        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 4948836            _decoratee.ReadAsync(buffer, cancellationToken) :
 4948837            PerformReadAsync();
 38
 39        async ValueTask<int> PerformReadAsync()
 4882340        {
 41            try
 4882342            {
 4882343                using CancellationTokenRegistration _ = cancellationToken.UnsafeRegister(
 28944                    cts => ((CancellationTokenSource)cts!).Cancel(),
 4882345                    _readCts);
 4882346                _readCts.CancelAfter(_idleTimeout); // enable idle timeout before reading
 47
 4882348                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.
 4835651                if (bytesRead > 0)
 4821952                {
 4821953                    ResetReadTimer();
 4821954                }
 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
 4835658                return bytesRead;
 59            }
 24060            catch (OperationCanceledException)
 24061            {
 24062                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
 4882369            {
 4882370                _readCts.CancelAfter(Timeout.InfiniteTimeSpan); // disable idle timeout if not canceled
 4882371            }
 4835672        }
 4948873    }
 74
 75    public Task ShutdownWriteAsync(CancellationToken cancellationToken) =>
 14376        _decoratee.ShutdownWriteAsync(cancellationToken);
 77
 78    public ValueTask WriteAsync(ReadOnlySequence<byte> buffer, CancellationToken cancellationToken)
 457379    {
 457380        return _idleTimeout == Timeout.InfiniteTimeSpan ?
 457381            _decoratee.WriteAsync(buffer, cancellationToken) :
 457382            PerformWriteAsync();
 83
 84        async ValueTask PerformWriteAsync()
 423585        {
 423586            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.
 422890            ResetWriteTimer();
 422891        }
 457392    }
 93
 94    /// <summary>Constructs a decorator that does nothing until it is enabled by a call to <see cref="Enable"/>.
 95    /// </summary>
 141096    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)
 357101        : this(decoratee)
 357102    {
 373103        _readTimer = new Timer(_ => sendReadPing());
 358104        _writeTimer = new Timer(_ => sendWritePing());
 357105    }
 106
 107    /// <summary>Sets the idle timeout and schedules pings once the connection is established.</summary>.
 108    internal void Enable(TimeSpan idleTimeout)
 627109    {
 627110        Debug.Assert(idleTimeout != Timeout.InfiniteTimeSpan);
 627111        _idleTimeout = idleTimeout;
 112
 627113        ResetReadTimer();
 627114        ResetWriteTimer();
 627115    }
 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>
 48846119    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.
 4855126    private void ResetWriteTimer() => _writeTimer?.Change(_idleTimeout * 0.6, Timeout.InfiniteTimeSpan);
 127}