| | | 1 | | // Copyright (c) ZeroC, Inc. |
| | | 2 | | |
| | | 3 | | using IceRpc.Internal; |
| | | 4 | | using IceRpc.Transports.Internal; |
| | | 5 | | using System.IO.Pipelines; |
| | | 6 | | |
| | | 7 | | namespace IceRpc.Transports.Slic.Internal; |
| | | 8 | | |
| | | 9 | | // The SlicPipeReader doesn't override ReadAtLeastAsyncCore or CopyToAsync methods because: |
| | | 10 | | // - we can't forward the calls to the internal pipe reader since reading relies on the AdvanceTo implementation to send |
| | | 11 | | // the StreamWindowUpdate frame once the data is examined, |
| | | 12 | | // - the default implementation can't be much optimized. |
| | | 13 | | internal class SlicPipeReader : PipeReader |
| | | 14 | | { |
| | | 15 | | private int _examined; |
| | | 16 | | private volatile Exception? _exception; |
| | | 17 | | private long _lastExaminedOffset; |
| | | 18 | | private readonly Pipe _pipe; |
| | | 19 | | private ReadResult _readResult; |
| | | 20 | | // FlagEnumExtensions operations are used to update the state. These operations are atomic and don't require mutex |
| | | 21 | | // locking. |
| | | 22 | | private int _state; |
| | | 23 | | private readonly SlicStream _stream; |
| | | 24 | | private int _windowSize; |
| | | 25 | | |
| | 3535 | 26 | | public override void AdvanceTo(SequencePosition consumed) => AdvanceTo(consumed, consumed); |
| | | 27 | | |
| | | 28 | | public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) |
| | 7447 | 29 | | { |
| | 7447 | 30 | | ThrowIfCompleted(); |
| | | 31 | | |
| | 7447 | 32 | | long startOffset = _readResult.Buffer.GetOffset(_readResult.Buffer.Start); |
| | 7447 | 33 | | long consumedOffset = _readResult.Buffer.GetOffset(consumed) - startOffset; |
| | 7447 | 34 | | long examinedOffset = _readResult.Buffer.GetOffset(examined) - startOffset; |
| | | 35 | | |
| | | 36 | | // Add the additional examined bytes to the examined bytes total. |
| | 7447 | 37 | | _examined += (int)(examinedOffset - _lastExaminedOffset); |
| | 7447 | 38 | | _lastExaminedOffset = examinedOffset - consumedOffset; |
| | | 39 | | |
| | | 40 | | // If the number of examined bytes is superior to the window update threshold, notifies the stream of the window |
| | | 41 | | // update. This will trigger the sending of a window update frame and allow the sender to send additional data. |
| | 7447 | 42 | | if (_examined >= _stream.WindowUpdateThreshold) |
| | 1252 | 43 | | { |
| | 1252 | 44 | | Interlocked.Add(ref _windowSize, _examined); |
| | 1252 | 45 | | _stream.WindowUpdate(_examined); |
| | 1252 | 46 | | _examined = 0; |
| | 1252 | 47 | | } |
| | | 48 | | |
| | 7447 | 49 | | _pipe.Reader.AdvanceTo(consumed, examined); |
| | 7447 | 50 | | } |
| | | 51 | | |
| | 5 | 52 | | public override void CancelPendingRead() => _pipe.Reader.CancelPendingRead(); |
| | | 53 | | |
| | | 54 | | public override void Complete(Exception? exception = null) |
| | 2785 | 55 | | { |
| | 2785 | 56 | | if (_state.TrySetFlag(State.Completed)) |
| | 2762 | 57 | | { |
| | | 58 | | // Forcefully close the stream reads if reads were not already gracefully closed by ReadAsync or TryRead. |
| | 2762 | 59 | | _stream.CloseReads(graceful: false); |
| | | 60 | | |
| | 2762 | 61 | | CompleteReads(exception: null); |
| | | 62 | | |
| | 2762 | 63 | | _pipe.Reader.Complete(); |
| | 2762 | 64 | | } |
| | 2785 | 65 | | } |
| | | 66 | | |
| | | 67 | | public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default) |
| | 7751 | 68 | | { |
| | 7751 | 69 | | ThrowIfCompleted(); |
| | | 70 | | |
| | 7751 | 71 | | if (_exception is not null) |
| | 16 | 72 | | { |
| | 16 | 73 | | _stream.ThrowIfConnectionClosed(); |
| | 13 | 74 | | } |
| | | 75 | | |
| | 7748 | 76 | | return ProcessReadResult(await _pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false)); |
| | 7464 | 77 | | } |
| | | 78 | | |
| | | 79 | | public override bool TryRead(out ReadResult result) |
| | 77 | 80 | | { |
| | 77 | 81 | | ThrowIfCompleted(); |
| | | 82 | | |
| | 77 | 83 | | if (_exception is not null) |
| | 0 | 84 | | { |
| | 0 | 85 | | _stream.ThrowIfConnectionClosed(); |
| | 0 | 86 | | } |
| | | 87 | | |
| | 77 | 88 | | if (_pipe.Reader.TryRead(out result)) |
| | 77 | 89 | | { |
| | 77 | 90 | | result = ProcessReadResult(result); |
| | 77 | 91 | | return true; |
| | | 92 | | } |
| | | 93 | | else |
| | 0 | 94 | | { |
| | 0 | 95 | | return false; |
| | | 96 | | } |
| | 77 | 97 | | } |
| | | 98 | | |
| | 2800 | 99 | | internal SlicPipeReader(SlicStream stream, SlicConnection connection) |
| | 2800 | 100 | | { |
| | 2800 | 101 | | _stream = stream; |
| | 2800 | 102 | | _windowSize = connection.InitialStreamWindowSize; |
| | | 103 | | |
| | | 104 | | // We keep the default readerScheduler (ThreadPool) because the _pipe.Writer.FlushAsync executes in the |
| | | 105 | | // "read loop task" and we don't want this task to continue into application code. The writerScheduler |
| | | 106 | | // doesn't matter since _pipe.Writer.FlushAsync never blocks. |
| | 2800 | 107 | | _pipe = new(new PipeOptions( |
| | 2800 | 108 | | pool: connection.Pool, |
| | 2800 | 109 | | pauseWriterThreshold: 0, |
| | 2800 | 110 | | minimumSegmentSize: connection.MinSegmentSize, |
| | 2800 | 111 | | useSynchronizationContext: false)); |
| | 2800 | 112 | | } |
| | | 113 | | |
| | | 114 | | /// <summary>Completes reads.</summary> |
| | | 115 | | /// <param name="exception">The exception that will be raised by <see cref="ReadAsync" /> or <see cref="TryRead" /> |
| | | 116 | | /// operation.</param> |
| | | 117 | | internal void CompleteReads(Exception? exception) |
| | 3136 | 118 | | { |
| | 3136 | 119 | | Interlocked.CompareExchange(ref _exception, exception, null); |
| | | 120 | | |
| | 3136 | 121 | | if (_state.TrySetFlag(State.PipeWriterCompleted)) |
| | 2797 | 122 | | { |
| | 2797 | 123 | | if (_state.HasFlag(State.PipeWriterInUse)) |
| | 3 | 124 | | { |
| | 3 | 125 | | _pipe.Reader.CancelPendingRead(); |
| | 3 | 126 | | } |
| | | 127 | | else |
| | 2794 | 128 | | { |
| | 2794 | 129 | | _pipe.Writer.Complete(exception); |
| | 2794 | 130 | | } |
| | 2797 | 131 | | } |
| | 3136 | 132 | | } |
| | | 133 | | |
| | | 134 | | /// <summary>Notifies the reader of the reception of a <see cref="FrameType.Stream" /> or <see |
| | | 135 | | /// cref="FrameType.StreamLast" /> frame. The stream data is consumed from the connection and buffered by this |
| | | 136 | | /// reader on its internal pipe.</summary> |
| | | 137 | | /// <returns><see langword="true" /> if the data was consumed; otherwise, <see langword="false"/> if the reader was |
| | | 138 | | /// completed by the application.</returns> |
| | | 139 | | internal async ValueTask<bool> ReceivedDataFrameAsync( |
| | | 140 | | int dataSize, |
| | | 141 | | bool endStream, |
| | | 142 | | CancellationToken cancellationToken) |
| | 8703 | 143 | | { |
| | 8703 | 144 | | if (dataSize == 0 && !endStream) |
| | 0 | 145 | | { |
| | 0 | 146 | | throw new IceRpcException( |
| | 0 | 147 | | IceRpcError.IceRpcError, |
| | 0 | 148 | | "An empty Slic stream frame is not allowed unless endStream is true."); |
| | | 149 | | } |
| | | 150 | | |
| | 8703 | 151 | | if (!_state.TrySetFlag(State.PipeWriterInUse)) |
| | 0 | 152 | | { |
| | 0 | 153 | | throw new InvalidOperationException( |
| | 0 | 154 | | $"The {nameof(ReceivedDataFrameAsync)} operation is not thread safe."); |
| | | 155 | | } |
| | | 156 | | |
| | | 157 | | try |
| | 8703 | 158 | | { |
| | 8703 | 159 | | if (_state.HasFlag(State.PipeWriterCompleted)) |
| | 0 | 160 | | { |
| | 0 | 161 | | return false; // No bytes consumed because the application completed the stream input. |
| | | 162 | | } |
| | | 163 | | |
| | 8703 | 164 | | int newWindowSize = Interlocked.Add(ref _windowSize, -dataSize); |
| | 8703 | 165 | | if (newWindowSize < 0) |
| | 0 | 166 | | { |
| | 0 | 167 | | throw new IceRpcException( |
| | 0 | 168 | | IceRpcError.IceRpcError, |
| | 0 | 169 | | "Received more data than flow control permits."); |
| | | 170 | | } |
| | | 171 | | |
| | | 172 | | // Fill the pipe writer with dataSize bytes. |
| | 8703 | 173 | | await _stream.FillBufferWriterAsync( |
| | 8703 | 174 | | _pipe.Writer, |
| | 8703 | 175 | | dataSize, |
| | 8703 | 176 | | cancellationToken).ConfigureAwait(false); |
| | | 177 | | |
| | 8703 | 178 | | if (endStream) |
| | 1287 | 179 | | { |
| | 1287 | 180 | | _pipe.Writer.Complete(); |
| | 1287 | 181 | | } |
| | | 182 | | else |
| | 7416 | 183 | | { |
| | 7416 | 184 | | _ = await _pipe.Writer.FlushAsync(CancellationToken.None).ConfigureAwait(false); |
| | 7416 | 185 | | } |
| | | 186 | | |
| | 8703 | 187 | | return true; |
| | | 188 | | } |
| | | 189 | | finally |
| | 8703 | 190 | | { |
| | 8703 | 191 | | if (_state.HasFlag(State.PipeWriterCompleted)) |
| | 3 | 192 | | { |
| | | 193 | | // If the pipe writer has been completed while we were reading the data from the stream, we make sure to |
| | | 194 | | // complete the writer now since Complete or CompleteWriter didn't do it. |
| | 3 | 195 | | _pipe.Writer.Complete(_exception); |
| | 3 | 196 | | } |
| | 8703 | 197 | | _state.ClearFlag(State.PipeWriterInUse); |
| | 8703 | 198 | | } |
| | 8703 | 199 | | } |
| | | 200 | | |
| | | 201 | | private ReadResult ProcessReadResult(ReadResult result) |
| | 7541 | 202 | | { |
| | | 203 | | // This method is called by ReadAsync or TryRead with the read result returned by the _pipe.Reader read |
| | | 204 | | // operation. |
| | 7541 | 205 | | if (result.IsCanceled) |
| | 5 | 206 | | { |
| | | 207 | | // The _pipe.Reader ReadAsync/TryRead operations can return a canceled read result for two different |
| | | 208 | | // reasons: |
| | | 209 | | // - the application called CancelPendingRead |
| | | 210 | | // - the connection is closed while data is written on _pipe.Writer |
| | 5 | 211 | | if (_state.HasFlag(State.PipeWriterCompleted)) |
| | 0 | 212 | | { |
| | | 213 | | // The connection was closed while the pipe writer was in use. Either throw or return a non-canceled |
| | | 214 | | // result depending on the completion exception. |
| | 0 | 215 | | if (_exception is null) |
| | 0 | 216 | | { |
| | 0 | 217 | | result = new ReadResult(result.Buffer, isCanceled: false, isCompleted: true); |
| | 0 | 218 | | } |
| | | 219 | | else |
| | 0 | 220 | | { |
| | 0 | 221 | | throw ExceptionUtil.Throw(_exception); |
| | | 222 | | } |
| | 0 | 223 | | } |
| | | 224 | | // else: the application called CancelPendingRead, return the canceled read result as-is. |
| | 5 | 225 | | } |
| | 7536 | 226 | | else if (result.IsCompleted) |
| | 1377 | 227 | | { |
| | | 228 | | // All the data from the peer is considered read at this point. It's time to close reads on the stream. This |
| | | 229 | | // will write the StreamReadsClosed frame to the peer and allow it to release the stream semaphore. |
| | 1377 | 230 | | _stream.CloseReads(graceful: true); |
| | 1377 | 231 | | } |
| | | 232 | | |
| | | 233 | | // Cache the read result returned to the application: AdvanceTo computes the consumed and examined offsets |
| | | 234 | | // against the cached buffer. |
| | 7541 | 235 | | _readResult = result; |
| | 7541 | 236 | | return result; |
| | 7541 | 237 | | } |
| | | 238 | | |
| | | 239 | | private void ThrowIfCompleted() |
| | 15275 | 240 | | { |
| | 15275 | 241 | | if (_state.HasFlag(State.Completed)) |
| | 0 | 242 | | { |
| | | 243 | | // If the reader is completed, the caller is bogus, it shouldn't call read operations after completing the |
| | | 244 | | // pipe reader. |
| | 0 | 245 | | throw new InvalidOperationException("Reading is not allowed once the reader is completed."); |
| | | 246 | | } |
| | 15275 | 247 | | } |
| | | 248 | | |
| | | 249 | | /// <summary>The state enumeration is used to ensure the reader is not used after it's completed and to ensure that |
| | | 250 | | /// the internal pipe writer isn't completed concurrently when it's being used by <see |
| | | 251 | | /// cref="ReceivedDataFrameAsync" />.</summary> |
| | | 252 | | private enum State : int |
| | | 253 | | { |
| | | 254 | | /// <summary><see cref="Complete" /> was called on this Slic pipe reader.</summary> |
| | | 255 | | Completed = 1, |
| | | 256 | | |
| | | 257 | | /// <summary>Data is being written to the internal pipe writer.</summary> |
| | | 258 | | PipeWriterInUse = 2, |
| | | 259 | | |
| | | 260 | | /// <summary>The internal pipe writer was completed by <see cref="CompleteReads" />.</summary> |
| | | 261 | | PipeWriterCompleted = 4, |
| | | 262 | | } |
| | | 263 | | } |