< Summary

Information
Class: IceRpc.Transports.Slic.Internal.SlicStream
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Transports/Slic/Internal/SlicStream.cs
Tag: 2300_35243572715
Line coverage
92%
Covered lines: 242
Uncovered lines: 20
Coverable lines: 262
Total lines: 515
Line coverage: 92.3%
Branch coverage
93%
Covered branches: 82
Total branches: 88
Branch coverage: 93.1%
Method coverage
100%
Covered methods: 29
Fully covered methods: 23
Total methods: 29
Method coverage: 100%
Full method coverage: 79.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Id()50%2271.42%
set_Id(...)100%11100%
get_Input()50%22100%
get_IsBidirectional()100%11100%
get_IsRemote()100%11100%
get_IsStarted()100%11100%
get_Output()50%22100%
get_WritesClosed()100%11100%
get_WindowUpdateThreshold()100%11100%
.ctor(...)100%1212100%
AcquireSendCreditAsync(...)100%11100%
Close(...)100%44100%
CloseReads(...)100%242492.5%
CloseWrites(...)100%171686.66%
ConsumedSendCredit(...)100%11100%
FillBufferWriterAsync(...)100%11100%
ReceivedDataFrameAsync(...)100%66100%
ReceivedReadsClosedFrame()75%4482.35%
ReceivedWindowUpdateFrame(...)75%4475%
ReceivedWritesClosedFrame()50%22100%
WindowUpdate(...)100%1172.72%
WriteStreamFrameAsync(...)100%22100%
WriteLastStreamFrameAsync()100%11100%
WroteLastStreamFrame()100%22100%
ThrowIfConnectionClosed()100%11100%
TrySetReadsClosed()100%11100%
TrySetWritesClosed()100%22100%
TrySetState(...)100%44100%
WriteStreamFrame(...)100%11100%

File(s)

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

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Transports.Internal;
 4using System.Buffers;
 5using System.Diagnostics;
 6using System.IO.Pipelines;
 7using ZeroC.Slice.Codec;
 8
 9namespace IceRpc.Transports.Slic.Internal;
 10
 11/// <summary>The stream implementation for Slic.</summary>
 12/// <remarks>The stream implementation implements flow control to ensure data isn't buffered indefinitely if the
 13/// application doesn't consume it.</remarks>
 14internal class SlicStream : IMultiplexedStream
 15{
 16    public ulong Id
 17    {
 18        get
 1782919        {
 1782920            ulong id = Volatile.Read(ref _id);
 1782921            if (id == ulong.MaxValue)
 022            {
 023                throw new InvalidOperationException("The stream ID isn't allocated yet.");
 24            }
 1782925            return id;
 1782926        }
 27
 28        set
 425329        {
 425330            Debug.Assert(_id == ulong.MaxValue);
 425331            Volatile.Write(ref _id, value);
 425332        }
 33    }
 34
 35    public PipeReader Input =>
 1358836        _inputPipeReader ?? throw new InvalidOperationException("A local unidirectional stream has no Input.");
 37
 38    /// <inheritdoc/>
 3635239    public bool IsBidirectional { get; }
 40
 41    /// <inheritdoc/>
 3379742    public bool IsRemote { get; }
 43
 44    /// <inheritdoc/>
 2557545    public bool IsStarted => Volatile.Read(ref _id) != ulong.MaxValue;
 46
 47    public PipeWriter Output =>
 916848        _outputPipeWriter ?? throw new InvalidOperationException("A remote unidirectional stream has no Output.");
 49
 228450    public Task WritesClosed => _writesClosedTcs.Task;
 51
 728352    internal int WindowUpdateThreshold => _connection.StreamWindowUpdateThreshold;
 53
 54    private bool _closeReadsOnWritesClosure;
 55    private readonly SlicConnection _connection;
 427656    private ulong _id = ulong.MaxValue;
 57    private readonly SlicPipeReader? _inputPipeReader;
 58    // This mutex protects _writesClosePending, _closeReadsOnWritesClosure.
 427659    private readonly Lock _mutex = new();
 60    private readonly SlicPipeWriter? _outputPipeWriter;
 61    // FlagEnumExtensions operations are used to update the state. These operations are atomic and don't require mutex
 62    // locking.
 63    private int _state;
 427664    private readonly TaskCompletionSource _writesClosedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
 65    private bool _writesClosePending;
 66
 427667    internal SlicStream(SlicConnection connection, bool isBidirectional, bool isRemote)
 427668    {
 427669        _connection = connection;
 70
 427671        IsBidirectional = isBidirectional;
 427672        IsRemote = isRemote;
 73
 427674        if (!IsBidirectional)
 287775        {
 287776            if (IsRemote)
 143477            {
 78                // Write-side of remote unidirectional stream is marked as closed.
 143479                TrySetWritesClosed();
 143480            }
 81            else
 144382            {
 83                // Read-side of local unidirectional stream is marked as closed.
 144384                TrySetReadsClosed();
 144385            }
 287786        }
 87
 427688        if (IsRemote || IsBidirectional)
 283389        {
 283390            _inputPipeReader = new SlicPipeReader(this, _connection);
 283391        }
 92
 427693        if (!IsRemote || IsBidirectional)
 284294        {
 284295            _outputPipeWriter = new SlicPipeWriter(this, _connection);
 284296        }
 427697    }
 98
 99    /// <summary>Acquires send credit.</summary>
 100    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 101    /// <returns>The available send credit.</returns>
 102    /// <remarks>This method should be called before sending a <see cref="FrameType.Stream"/> or <see
 103    /// cref="FrameType.StreamLast"/> frame to ensure enough send credit is available. If no send credit is available,
 104    /// it will block until send credit is available. The send credit matches the size of the peer's flow-control
 105    /// window.</remarks>
 106    internal ValueTask<int> AcquireSendCreditAsync(CancellationToken cancellationToken) =>
 9298107        _outputPipeWriter!.AcquireSendCreditAsync(cancellationToken);
 108
 109    /// <summary>Closes the read and write sides of the stream and notifies the stream <see cref="Input" /> and <see
 110    /// cref="Output" /> of the reads and writes closure.</summary>
 111    internal void Close(Exception closeException)
 747112    {
 747113        if (TrySetReadsClosed())
 360114        {
 360115            Debug.Assert(_inputPipeReader is not null);
 360116            _inputPipeReader.CompleteReads(closeException);
 360117        }
 747118        if (TrySetWritesClosed())
 426119        {
 426120            Debug.Assert(_outputPipeWriter is not null);
 426121            _outputPipeWriter.CompleteWrites(closeException);
 426122        }
 747123    }
 124
 125    /// <summary>Closes the read-side of the stream. It's only called by <see cref="SlicPipeReader.Complete" />, <see
 126    /// cref="SlicPipeReader.TryRead" /> or <see cref="SlicPipeReader.ReadAsync" /> and never called concurrently.
 127    /// </summary>
 128    /// <param name="graceful"><see langword="true" /> if the application consumed all the stream data from the stream
 129    /// <see cref="Input" />; otherwise, <see langword="false" />.</param>
 130    internal void CloseReads(bool graceful)
 4773131    {
 4773132        bool writeReadsClosedFrame = false;
 133
 134        lock (_mutex)
 4773135        {
 4773136            if (IsStarted && !_state.HasFlag(State.ReadsClosed))
 2410137            {
 138                // As an optimization, if reads are gracefully closed once the buffered data is consumed but before
 139                // writes are closed, we don't send the StreamReadsClosed frame just yet. Instead, when writes are
 140                // closed, CloseWrites will bundle the sending of the StreamReadsClosed with the sending of the
 141                // StreamLast or StreamWritesClosed frame. This allows to send both frames with a single write on the
 142                // duplex connection. If the peer closes its reads first, ReceivedReadsClosedFrame sends the frame on
 143                // its own.
 2410144                if (graceful &&
 2410145                    IsBidirectional &&
 2410146                    IsRemote &&
 2410147                    !_state.HasFlag(State.WritesClosed) &&
 2410148                    !_writesClosePending)
 498149                {
 498150                    _closeReadsOnWritesClosure = true;
 498151                }
 1912152                else if (!graceful || IsRemote)
 1344153                {
 154                    // If forcefully closed because the input was completed before the data was fully read or if writes
 155                    // are already closed and the stream is a remote stream, we send the StreamReadsClosed frame to
 156                    // notify the peer that reads are closed.
 1344157                    writeReadsClosedFrame = true;
 1344158                }
 2410159            }
 4773160        }
 161
 4773162        if (writeReadsClosedFrame)
 1344163        {
 1344164            if (IsRemote)
 1266165            {
 166                // If it's a remote stream, we close writes before sending the StreamReadsClosed frame to ensure
 167                // _connection._bidirectionalStreamCount or _connection._unidirectionalStreamCount is decreased before
 168                // the peer receives the frame. This is necessary to prevent a race condition where the peer could
 169                // release the connection's bidirectional or unidirectional stream semaphore before this connection's
 170                // stream count is actually decreased.
 1266171                TrySetReadsClosed();
 1266172            }
 173
 174            try
 1344175            {
 1344176                WriteStreamFrame(FrameType.StreamReadsClosed, encode: null, writeReadsClosedFrame: false);
 1344177            }
 0178            catch (IceRpcException)
 0179            {
 180                // Ignore connection failures.
 0181            }
 182
 1344183            if (!IsRemote)
 78184            {
 185                // We can now close reads to permit a new stream to be started. The peer will receive the
 186                // StreamReadsClosed frame before the new stream sends a Stream frame.
 78187                TrySetReadsClosed();
 78188            }
 1344189        }
 190        else
 3429191        {
 3429192            TrySetReadsClosed();
 3429193        }
 4773194    }
 195
 196    /// <summary>Closes the write-side of the stream. It's only called by <see cref="SlicPipeWriter.Complete" /> and
 197    /// never called concurrently.</summary>
 198    /// <param name="graceful"><see langword="true" /> if the application wrote all the stream data on the stream <see
 199    /// cref="Output" />; otherwise, <see langword="false" />.</param>
 200    internal void CloseWrites(bool graceful)
 2811201    {
 2811202        bool writeWritesClosedFrame = false;
 2811203        bool writeReadsClosedFrame = false;
 204
 205        lock (_mutex)
 2811206        {
 2811207            if (IsStarted && !_state.HasFlag(State.WritesClosed) && !_writesClosePending)
 645208            {
 209                // The frame written below can't be canceled, so it claims the deferred StreamReadsClosed frame. This
 210                // also keeps ReceivedReadsClosedFrame from sending it before the stream is released.
 645211                writeReadsClosedFrame = _closeReadsOnWritesClosure;
 645212                _closeReadsOnWritesClosure = false;
 645213                _writesClosePending = true;
 645214                writeWritesClosedFrame = true;
 645215            }
 2811216        }
 217
 2811218        if (writeWritesClosedFrame)
 645219        {
 645220            if (IsRemote)
 257221            {
 222                // If it's a remote stream, we close writes before sending the StreamLast or StreamWritesClosed
 223                // frame to ensure _connection._bidirectionalStreamCount or _connection._unidirectionalStreamCount
 224                // is decreased before the peer receives the frame. This is necessary to prevent a race condition
 225                // where the peer could release the connection's bidirectional or unidirectional stream semaphore
 226                // before this connection's stream count is actually decreased.
 257227                TrySetWritesClosed();
 257228            }
 229
 645230            if (graceful)
 594231            {
 232                try
 594233                {
 594234                    WriteStreamFrame(FrameType.StreamLast, encode: null, writeReadsClosedFrame);
 594235                }
 0236                catch (IceRpcException)
 0237                {
 238                    // Ignore connection failures.
 0239                }
 240
 241                // If the stream is a local stream, writes are not closed until the StreamReadsClosed frame is
 242                // received from the peer (see ReceivedReadsClosedFrame). This ensures that the connection's
 243                // bidirectional or unidirectional stream semaphore is released only once the peer consumed the
 244                // buffered data.
 594245            }
 246            else
 51247            {
 248                try
 51249                {
 51250                    WriteStreamFrame(FrameType.StreamWritesClosed, encode: null, writeReadsClosedFrame);
 51251                }
 0252                catch (IceRpcException)
 0253                {
 254                    // Ignore connection failures.
 0255                }
 256
 51257                if (!IsRemote)
 28258                {
 259                    // We can now close writes to allow starting a new stream. Since the sending of frames is
 260                    // serialized over the connection, the peer will receive this StreamWritesClosed frame before
 261                    // a new stream sends a StreamFrame frame.
 28262                    TrySetWritesClosed();
 28263                }
 51264            }
 645265        }
 2166266        else if (!IsStarted)
 18267        {
 268            // The peer knows nothing about an unstarted stream, so its writes are closed right away.
 18269            TrySetWritesClosed();
 18270        }
 271        // Otherwise, the stream either already closed writes or, for a local stream, queued a StreamLast frame. Once
 272        // the peer's StreamReadsClosed frame is received (see ReceivedReadsClosedFrame), it closes writes and releases
 273        // its stream-count permit.
 2811274    }
 275
 276    /// <summary>Notifies the stream of the amount of data consumed by the connection to send a <see
 277    /// cref="FrameType.Stream" /> or <see cref="FrameType.StreamLast" /> frame.</summary>
 278    /// <param name="size">The size of the stream frame.</param>
 8287279    internal void ConsumedSendCredit(int size) => _outputPipeWriter!.ConsumedSendCredit(size);
 280
 281    /// <summary>Fills the given writer with stream data received on the connection.</summary>
 282    /// <param name="bufferWriter">The destination buffer writer.</param>
 283    /// <param name="byteCount">The amount of stream data to read.</param>
 284    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 285    internal ValueTask FillBufferWriterAsync(
 286        IBufferWriter<byte> bufferWriter,
 287        int byteCount,
 288        CancellationToken cancellationToken) =>
 8758289        _connection.FillBufferWriterAsync(bufferWriter, byteCount, cancellationToken);
 290
 291    /// <summary>Notifies the stream of the reception of a <see cref="FrameType.Stream" /> or <see
 292    /// cref="FrameType.StreamLast" /> frame.</summary>
 293    /// <param name="size">The size of the data carried by the stream frame.</param>
 294    /// <param name="endStream"><see langword="true" /> if the received stream frame is the <see
 295    /// cref="FrameType.StreamLast" /> frame; otherwise, <see langword="false" />.</param>
 296    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 297    internal ValueTask<bool> ReceivedDataFrameAsync(int size, bool endStream, CancellationToken cancellationToken)
 8775298    {
 8775299        Debug.Assert(_inputPipeReader is not null);
 8775300        if (_state.HasFlag(State.ReadsClosed))
 17301        {
 17302            return new(false);
 303        }
 304        else
 8758305        {
 8758306            if (endStream && !IsRemote)
 568307            {
 308                // For a local stream we can close reads after we have received the StreamLast frame. For remote
 309                // streams reads are closed after the application has consumed all the data.
 568310                CloseReads(graceful: true);
 568311            }
 8758312            return _inputPipeReader.ReceivedDataFrameAsync(size, endStream, cancellationToken);
 313        }
 8775314    }
 315
 316    /// <summary>Notifies the stream of the reception of a <see cref="FrameType.StreamReadsClosed" /> frame.</summary>
 317    internal void ReceivedReadsClosedFrame()
 1723318    {
 319        // Writes are closed before the deferral is captured: a CloseReads that runs afterwards sees writes closed and
 320        // sends the StreamReadsClosed frame itself.
 1723321        TrySetWritesClosed();
 322
 323        bool writeReadsClosedFrame;
 324        lock (_mutex)
 1723325        {
 326            // A pending StreamLast write may no longer carry the deferred StreamReadsClosed frame, so it's sent on its
 327            // own.
 1723328            writeReadsClosedFrame = _closeReadsOnWritesClosure;
 1723329            _closeReadsOnWritesClosure = false;
 1723330        }
 331
 1723332        _outputPipeWriter?.CompleteWrites(exception: null);
 333
 1723334        if (writeReadsClosedFrame)
 10335        {
 336            try
 10337            {
 10338                WriteStreamFrame(FrameType.StreamReadsClosed, encode: null, writeReadsClosedFrame: false);
 10339            }
 0340            catch (IceRpcException)
 0341            {
 342                // Ignore connection failures.
 0343            }
 10344        }
 1723345    }
 346
 347    /// <summary>Notifies the stream of the reception of a <see cref="FrameType.StreamWindowUpdate" /> frame.</summary>
 348    /// <param name="frame">The body of the <see cref="FrameType.StreamWindowUpdate" /> frame.</param>
 349    internal void ReceivedWindowUpdateFrame(StreamWindowUpdateBody frame)
 1198350    {
 351        // The connection rejects window updates on remote unidirectional streams, the only streams with no output
 352        // pipe writer.
 1198353        Debug.Assert(_outputPipeWriter is not null);
 354
 1198355        if (frame.WindowSizeIncrement == 0)
 1356        {
 1357            throw new InvalidDataException(
 1358                $"Received {nameof(FrameType.StreamWindowUpdate)} frame with a zero window size increment.");
 359        }
 1197360        if (frame.WindowSizeIncrement > SlicTransportOptions.MaxWindowSize)
 0361        {
 0362            throw new InvalidDataException(
 0363                "The window update is trying to increase the window size to a value larger than allowed.");
 364        }
 1197365        _outputPipeWriter.ReceivedWindowUpdateFrame((int)frame.WindowSizeIncrement);
 1196366    }
 367
 368    /// <summary>Notifies the stream of the reception of a <see cref="FrameType.StreamWritesClosed" /> frame.</summary>
 369    internal void ReceivedWritesClosedFrame()
 49370    {
 49371        TrySetReadsClosed();
 372
 373        // Read operations will return a TruncatedData error if the peer closed writes.
 49374        _inputPipeReader?.CompleteReads(new IceRpcException(IceRpcError.TruncatedData));
 49375    }
 376
 377    /// <summary>Notifies the stream of the window update.</summary>
 378    /// <param name="size">The amount of data consumed by the application on the stream <see cref="Input" />.</param>
 379    internal void WindowUpdate(int size)
 1248380    {
 381        try
 1248382        {
 383            // Notify the sender of the window update to permit the sending of additional data.
 1248384            WriteStreamFrame(
 1248385                FrameType.StreamWindowUpdate,
 1248386                new StreamWindowUpdateBody((ulong)size).Encode,
 1248387                writeReadsClosedFrame: false);
 1248388        }
 0389        catch (IceRpcException)
 0390        {
 391            // Ignore connection failures.
 0392        }
 1248393    }
 394
 395    /// <summary>Writes a <see cref="FrameType.Stream" /> or <see cref="FrameType.StreamLast" /> frame on the
 396    /// connection.</summary>
 397    /// <param name="source1">The first stream frame data source.</param>
 398    /// <param name="source2">The second stream frame data source.</param>
 399    /// <param name="endStream"><see langword="true" /> to write a <see cref="FrameType.StreamLast" /> frame; otherwise,
 400    /// <see langword="false" />.</param>
 401    /// <param name="cancellationToken">A cancellation token that receives the cancellation requests.</param>
 402    internal ValueTask<FlushResult> WriteStreamFrameAsync(
 403        ReadOnlySequence<byte> source1,
 404        ReadOnlySequence<byte> source2,
 405        bool endStream,
 406        CancellationToken cancellationToken)
 7985407    {
 7985408        if (!endStream)
 6172409        {
 410            // Hot path: a non-final stream frame requires no writes-closure bookkeeping, so forward the inner
 411            // ValueTask directly without an async state machine.
 6172412            return _connection.WriteStreamDataFrameAsync(
 6172413                this,
 6172414                source1,
 6172415                source2,
 6172416                endStream: false,
 6172417                writeReadsClosedFrame: false,
 6172418                cancellationToken);
 419        }
 420
 1813421        return WriteLastStreamFrameAsync();
 422
 423        async ValueTask<FlushResult> WriteLastStreamFrameAsync()
 1813424        {
 425            bool writeReadsClosedFrame;
 426            lock (_mutex)
 1813427            {
 1813428                writeReadsClosedFrame = _closeReadsOnWritesClosure;
 1813429                _writesClosePending = true;
 1813430            }
 431
 432            try
 1813433            {
 1813434                return await _connection.WriteStreamDataFrameAsync(
 1813435                    this,
 1813436                    source1,
 1813437                    source2,
 1813438                    endStream: true,
 1813439                    writeReadsClosedFrame,
 1813440                    cancellationToken).ConfigureAwait(false);
 441            }
 1007442            catch when (!_writesClosedTcs.Task.IsCompleted)
 2443            {
 444                // The write failed before the StreamLast frame was queued on the connection (WroteLastStreamFrame
 445                // completes _writesClosedTcs when the frame is queued), so roll back _writesClosePending so that
 446                // completing the stream output can still close writes and notify the peer with a StreamWritesClosed
 447                // frame. The bundled reads closure isn't lost either since _closeReadsOnWritesClosure wasn't cleared.
 448                lock (_mutex)
 2449                {
 2450                    _writesClosePending = false;
 2451                }
 2452                throw;
 453            }
 806454        }
 7985455    }
 456
 457    /// <summary>Notifies the stream that the <see cref="FrameType.StreamLast" /> was written by the
 458    /// connection.</summary>
 459    internal void WroteLastStreamFrame()
 1401460    {
 1401461        if (IsRemote)
 624462        {
 624463            TrySetWritesClosed();
 624464        }
 465        // For local streams, writes will be closed only once the peer sends the StreamReadsClosed frame.
 466
 1401467        _writesClosedTcs.TrySetResult();
 1401468    }
 469
 470    /// <summary>Throws the connection closure exception if the connection is closed.</summary>
 8012471    internal void ThrowIfConnectionClosed() => _connection.ThrowIfClosed();
 472
 7012473    private bool TrySetReadsClosed() => TrySetState(State.ReadsClosed);
 474
 475    private bool TrySetWritesClosed()
 4831476    {
 4831477        if (TrySetState(State.WritesClosed))
 4271478        {
 4271479            _writesClosedTcs.TrySetResult();
 4271480            return true;
 481        }
 482        else
 560483        {
 560484            return false;
 485        }
 4831486    }
 487
 488    private bool TrySetState(State state)
 11843489    {
 11843490        if (_state.TrySetFlag(state, out int newState))
 8545491        {
 8545492            if (newState.HasFlag(State.ReadsClosed | State.WritesClosed))
 4271493            {
 494                // The stream reads and writes are closed, it's time to release the stream to either allow creating or
 495                // accepting a new stream.
 4271496                _connection.ReleaseStream(this);
 4271497            }
 8545498            return true;
 499        }
 500        else
 3298501        {
 3298502            return false;
 503        }
 11843504    }
 505
 506    private void WriteStreamFrame(FrameType frameType, EncodeAction? encode, bool writeReadsClosedFrame) =>
 3247507        _connection.WriteStreamFrame(stream: this, frameType, encode, writeReadsClosedFrame);
 508
 509    [Flags]
 510    private enum State : int
 511    {
 512        ReadsClosed = 1,
 513        WritesClosed = 2
 514    }
 515}