| | | 1 | | // Copyright (c) ZeroC, Inc. |
| | | 2 | | |
| | | 3 | | using System.Diagnostics; |
| | | 4 | | using System.Diagnostics.CodeAnalysis; |
| | | 5 | | using System.IO.Pipelines; |
| | | 6 | | using System.Net; |
| | | 7 | | using System.Threading.Channels; |
| | | 8 | | |
| | | 9 | | using ConnectRequest = ( |
| | | 10 | | System.Threading.Tasks.TaskCompletionSource<System.IO.Pipelines.PipeReader> Tcs, |
| | | 11 | | System.IO.Pipelines.PipeReader ClientPipeReader, |
| | | 12 | | System.Threading.CancellationTokenRegistration Registration); |
| | | 13 | | |
| | | 14 | | namespace IceRpc.Transports.Coloc.Internal; |
| | | 15 | | |
| | | 16 | | /// <summary>The listener implementation for the colocated transport.</summary> |
| | | 17 | | internal class ColocListener : IListener<IDuplexConnection>, IDisposable |
| | | 18 | | { |
| | 1080 | 19 | | public TransportAddress TransportAddress { get; } |
| | | 20 | | |
| | | 21 | | [SuppressMessage( |
| | | 22 | | "Usage", |
| | | 23 | | "CA2213:Disposable fields should be disposed", |
| | | 24 | | Justification = "Disposing this CTS races with AcceptAsync creating a linked token source from its Token; a CTS |
| | 575 | 25 | | private readonly CancellationTokenSource _disposeCts = new(); |
| | | 26 | | private bool _disposed; |
| | | 27 | | private readonly Action<ColocListener> _onDispose; |
| | 575 | 28 | | private readonly Lock _mutex = new(); |
| | | 29 | | private readonly EndPoint _networkAddress; |
| | | 30 | | private readonly PipeOptions _pipeOptions; |
| | | 31 | | |
| | | 32 | | // The channel used by the client connection ConnectAsync method to queue a connection establishment request. A |
| | | 33 | | // client connection establishment request is represented by: |
| | | 34 | | // - a TaskCompletionSource which is completed by AcceptAsync when the connection is accepted. The server connection |
| | | 35 | | // pipe reader is set as the result. ClientColocConnection.ConnectAsync waits on the task completion source task. |
| | | 36 | | // - the client connection pipe reader provided to the server connection when the server connection is created by |
| | | 37 | | // AcceptAsync. |
| | | 38 | | // - the cancellation token registration that cancels the TaskCompletionSource; it's disposed when the request is |
| | | 39 | | // dequeued. |
| | | 40 | | private readonly Channel<ConnectRequest> _channel; |
| | | 41 | | |
| | | 42 | | public async Task<(IDuplexConnection, EndPoint)> AcceptAsync(CancellationToken cancellationToken) |
| | 610 | 43 | | { |
| | | 44 | | CancellationTokenSource cts; |
| | | 45 | | lock (_mutex) |
| | 610 | 46 | | { |
| | 610 | 47 | | ObjectDisposedException.ThrowIf(_disposed, this); |
| | 608 | 48 | | cts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, cancellationToken); |
| | 608 | 49 | | } |
| | 608 | 50 | | using var _ = cts; |
| | | 51 | | try |
| | 608 | 52 | | { |
| | 608 | 53 | | while (true) |
| | 608 | 54 | | { |
| | 608 | 55 | | ConnectRequest request = await _channel.Reader.ReadAsync(cts.Token).ConfigureAwait(false); |
| | 527 | 56 | | request.Registration.Dispose(); |
| | | 57 | | |
| | 527 | 58 | | var serverPipe = new Pipe(_pipeOptions); |
| | 527 | 59 | | if (request.Tcs.TrySetResult(serverPipe.Reader)) |
| | 527 | 60 | | { |
| | 527 | 61 | | var serverConnection = new ServerColocConnection( |
| | 527 | 62 | | TransportAddress, |
| | 527 | 63 | | serverPipe.Writer, |
| | 527 | 64 | | request.ClientPipeReader); |
| | 527 | 65 | | return (serverConnection, _networkAddress); |
| | | 66 | | } |
| | | 67 | | else |
| | 0 | 68 | | { |
| | | 69 | | // The client connection establishment was canceled. |
| | 0 | 70 | | serverPipe.Writer.Complete(); |
| | 0 | 71 | | serverPipe.Reader.Complete(); |
| | 0 | 72 | | } |
| | 0 | 73 | | } |
| | | 74 | | } |
| | 81 | 75 | | catch (OperationCanceledException) |
| | 81 | 76 | | { |
| | 81 | 77 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 78 | | // The accept operation was canceled because the listener was disposed. |
| | 11 | 79 | | Debug.Assert(_disposeCts.IsCancellationRequested); |
| | 11 | 80 | | throw new ObjectDisposedException($"{typeof(ColocListener)}"); |
| | | 81 | | } |
| | 527 | 82 | | } |
| | | 83 | | |
| | | 84 | | public void Dispose() |
| | 1022 | 85 | | { |
| | | 86 | | lock (_mutex) |
| | 1022 | 87 | | { |
| | 1022 | 88 | | if (_disposed) |
| | 447 | 89 | | { |
| | 447 | 90 | | return; |
| | | 91 | | } |
| | 575 | 92 | | _disposed = true; |
| | | 93 | | |
| | | 94 | | // Notify the owner (e.g. the server transport) so it can release its reference to this listener. |
| | 575 | 95 | | _onDispose(this); |
| | | 96 | | |
| | | 97 | | // Cancel pending AcceptAsync. |
| | 575 | 98 | | _disposeCts.Cancel(); |
| | | 99 | | |
| | | 100 | | // Ensure no more client connection establishment request is queued. |
| | 575 | 101 | | _channel.Writer.Complete(); |
| | | 102 | | |
| | | 103 | | // Complete all the queued client connection establishment requests with IceRpcError.ConnectionRefused. |
| | | 104 | | // Use TrySetException in case the task has been already canceled. |
| | 594 | 105 | | while (_channel.Reader.TryRead(out ConnectRequest item)) |
| | 19 | 106 | | { |
| | 19 | 107 | | item.Registration.Dispose(); |
| | 19 | 108 | | item.Tcs.TrySetException(new IceRpcException(IceRpcError.ConnectionRefused)); |
| | 19 | 109 | | } |
| | 575 | 110 | | } |
| | 1022 | 111 | | } |
| | | 112 | | |
| | | 113 | | public ValueTask DisposeAsync() |
| | 1020 | 114 | | { |
| | 1020 | 115 | | Dispose(); |
| | 1020 | 116 | | return default; |
| | 1020 | 117 | | } |
| | | 118 | | |
| | 575 | 119 | | internal ColocListener( |
| | 575 | 120 | | TransportAddress transportAddress, |
| | 575 | 121 | | Action<ColocListener> onDispose, |
| | 575 | 122 | | ColocTransportOptions colocTransportOptions, |
| | 575 | 123 | | DuplexConnectionOptions duplexConnectionOptions) |
| | 575 | 124 | | { |
| | 575 | 125 | | TransportAddress = transportAddress; |
| | | 126 | | |
| | 575 | 127 | | _onDispose = onDispose; |
| | 575 | 128 | | _networkAddress = new ColocEndPoint(transportAddress); |
| | 575 | 129 | | _pipeOptions = new PipeOptions( |
| | 575 | 130 | | pool: duplexConnectionOptions.Pool, |
| | 575 | 131 | | minimumSegmentSize: duplexConnectionOptions.MinSegmentSize, |
| | 575 | 132 | | pauseWriterThreshold: colocTransportOptions.PauseWriterThreshold, |
| | 575 | 133 | | resumeWriterThreshold: colocTransportOptions.ResumeWriterThreshold, |
| | 575 | 134 | | useSynchronizationContext: false); |
| | | 135 | | |
| | | 136 | | // Create a bounded channel with a capacity that matches the listen backlog, and with |
| | | 137 | | // the default concurrency settings that allow multiple reader and writers. |
| | 575 | 138 | | _channel = Channel.CreateBounded<ConnectRequest>( |
| | 575 | 139 | | new BoundedChannelOptions(colocTransportOptions.ListenBacklog)); |
| | 575 | 140 | | } |
| | | 141 | | |
| | | 142 | | /// <summary>Queue client connection establishment requests from the client.</summary> |
| | | 143 | | /// <param name="clientPipeReader">A <see cref="PipeReader"/> for reading from the client connection.</param> |
| | | 144 | | /// <param name="cancellationToken">>A cancellation token that receives the cancellation requests.</param> |
| | | 145 | | /// <param name="serverPipeReaderTask">A task that returns a <see cref="PipeReader"/> for reading from the server |
| | | 146 | | /// connection.</param> |
| | | 147 | | /// <returns>Returns true if the connection establishment request has been queue otherwise, false.</returns> |
| | | 148 | | internal bool TryQueueConnect( |
| | | 149 | | PipeReader clientPipeReader, |
| | | 150 | | CancellationToken cancellationToken, |
| | | 151 | | [NotNullWhen(true)] out Task<PipeReader>? serverPipeReaderTask) |
| | 549 | 152 | | { |
| | | 153 | | // Create a tcs that is completed by AcceptAsync when accepts the corresponding connection, at which point |
| | | 154 | | // the client side connect operation will complete. |
| | | 155 | | // We use RunContinuationsAsynchronously to avoid the ConnectAsync continuation end up running in the AcceptAsyn |
| | | 156 | | // loop that completes this tcs. |
| | 549 | 157 | | var tcs = new TaskCompletionSource<PipeReader>(TaskCreationOptions.RunContinuationsAsynchronously); |
| | 549 | 158 | | CancellationTokenRegistration registration = |
| | 556 | 159 | | cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); |
| | 549 | 160 | | if (_channel.Writer.TryWrite((tcs, clientPipeReader, registration))) |
| | 546 | 161 | | { |
| | 546 | 162 | | serverPipeReaderTask = tcs.Task; |
| | 546 | 163 | | return true; |
| | | 164 | | } |
| | | 165 | | else |
| | 3 | 166 | | { |
| | 3 | 167 | | registration.Dispose(); |
| | 3 | 168 | | serverPipeReaderTask = null; |
| | 3 | 169 | | return false; |
| | | 170 | | } |
| | 549 | 171 | | } |
| | | 172 | | } |