Skip to content

Commit 37eff15

Browse files
authored
[QUIC] Root listener/connection while waiting on new connection/stream event (#74450)
* Fixed GC collecting listener and/or connection while waiting on new connection/stream event * Minor fixes and cleanups
1 parent 38ba41e commit 37eff15

File tree

8 files changed

+70
-20
lines changed

8 files changed

+70
-20
lines changed

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,6 @@
446446
<Reference Include="System.Diagnostics.DiagnosticSource" />
447447
<Reference Include="System.Diagnostics.Tracing" />
448448
<Reference Include="System.IO.Compression" />
449-
<Reference Include="System.Linq" />
450449
<Reference Include="System.Memory" />
451450
<Reference Include="System.Net.NameResolution" />
452451
<Reference Include="System.Net.NetworkInformation" />

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Runtime.Versioning;
88
using System.Net.Quic;
99
using System.IO;
10-
using System.Linq;
1110
using System.Collections.Generic;
1211
using System.Diagnostics;
1312
using System.Globalization;
@@ -155,7 +154,7 @@ private void CheckForShutdown()
155154

156155
if (_clientControl != null)
157156
{
158-
_clientControl.Dispose();
157+
await _clientControl.DisposeAsync().ConfigureAwait(false);
159158
_clientControl = null;
160159
}
161160

@@ -245,7 +244,10 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon
245244
}
246245
finally
247246
{
248-
requestStream?.Dispose();
247+
if (requestStream is not null)
248+
{
249+
await requestStream.DisposeAsync().ConfigureAwait(false);
250+
}
249251
}
250252
}
251253

@@ -562,7 +564,6 @@ private async Task ProcessServerStreamAsync(QuicStream stream)
562564
}
563565

564566
stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.StreamCreationError);
565-
stream.Dispose();
566567
return;
567568
}
568569
}

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ public async ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken ca
392392
throw new InvalidOperationException(SR.net_quic_accept_not_allowed);
393393
}
394394

395+
GCHandle keepObject = GCHandle.Alloc(this);
395396
try
396397
{
397398
return await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
@@ -401,6 +402,10 @@ public async ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken ca
401402
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
402403
throw;
403404
}
405+
finally
406+
{
407+
keepObject.Free();
408+
}
404409
}
405410

406411
/// <summary>

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ public async ValueTask<QuicConnection> AcceptConnectionAsync(CancellationToken c
162162
{
163163
ObjectDisposedException.ThrowIf(_disposed == 1, this);
164164

165+
GCHandle keepObject = GCHandle.Alloc(this);
165166
try
166167
{
167168
PendingConnection pendingConnection = await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
@@ -175,6 +176,10 @@ public async ValueTask<QuicConnection> AcceptConnectionAsync(CancellationToken c
175176
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
176177
throw;
177178
}
179+
finally
180+
{
181+
keepObject.Free();
182+
}
178183
}
179184

180185
private unsafe int HandleEventNewConnection(ref NEW_CONNECTION_DATA data)

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ public void Abort(QuicAbortDirection abortDirection, long errorCode)
398398
QUIC_STREAM_SHUTDOWN_FLAGS flags = QUIC_STREAM_SHUTDOWN_FLAGS.NONE;
399399
if (abortDirection.HasFlag(QuicAbortDirection.Read))
400400
{
401-
flags |= QUIC_STREAM_SHUTDOWN_FLAGS.ABORT_RECEIVE;
402401
if (_receiveTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_reading_aborted), final: true))
403402
{
404403
flags |= QUIC_STREAM_SHUTDOWN_FLAGS.ABORT_RECEIVE;
@@ -537,12 +536,12 @@ private unsafe int HandleEventShutdownComplete(ref SHUTDOWN_COMPLETE data)
537536
(shutdownByApp: true, closedRemotely: true) => ThrowHelper.GetConnectionAbortedException((long)data.ConnectionErrorCode),
538537
// It's local shutdown by app, this side called QuicConnection.CloseAsync, throw QuicError.OperationAborted.
539538
(shutdownByApp: true, closedRemotely: false) => ThrowHelper.GetOperationAbortedException(),
540-
// It's remote shutdown by transport, we received a CONNECTION_CLOSE frame with a QUIC transport error code
539+
// It's remote shutdown by transport, we received a CONNECTION_CLOSE frame with a QUIC transport error code, throw error based on the status.
541540
// TODO: we should propagate the transport error code
542541
// https://github.com/dotnet/runtime/issues/72666
543542
(shutdownByApp: false, closedRemotely: true) => ThrowHelper.GetExceptionForMsQuicStatus(data.ConnectionCloseStatus, $"Shutdown by transport {data.ConnectionErrorCode}"),
544-
// It's local shutdown by transport, due to some timeout
545-
// TODO: we should propagate transport error code
543+
// It's local shutdown by transport, most likely due to a timeout, throw error based on the status.
544+
// TODO: we should propagate the transport error code
546545
// https://github.com/dotnet/runtime/issues/72666
547546
(shutdownByApp: false, closedRemotely: false) => ThrowHelper.GetExceptionForMsQuicStatus(data.ConnectionCloseStatus),
548547
};

src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,5 +336,33 @@ public async Task Connect_PeerCertificateDisposed(bool useGetter)
336336
}
337337
peerCertificate.Dispose();
338338
}
339+
340+
[Fact]
341+
public async Task Connection_AwaitsStream_ConnectionSurvivesGC()
342+
{
343+
const byte data = 0xDC;
344+
345+
TaskCompletionSource<IPEndPoint> listenerEndpointTcs = new TaskCompletionSource<IPEndPoint>();
346+
await Task.WhenAll(
347+
Task.Run(async () =>
348+
{
349+
await using var listener = await CreateQuicListener();
350+
listenerEndpointTcs.SetResult(listener.LocalEndPoint);
351+
await using var connection = await listener.AcceptConnectionAsync();
352+
await using var stream = await connection.AcceptInboundStreamAsync();
353+
var buffer = new byte[1];
354+
Assert.Equal(1, await stream.ReadAsync(buffer));
355+
Assert.Equal(data, buffer[0]);
356+
}).WaitAsync(TimeSpan.FromSeconds(5)),
357+
Task.Run(async () =>
358+
{
359+
var endpoint = await listenerEndpointTcs.Task;
360+
await using var connection = await CreateQuicConnection(endpoint);
361+
await Task.Delay(TimeSpan.FromSeconds(0.5));
362+
GC.Collect();
363+
await using var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Unidirectional);
364+
await stream.WriteAsync(new byte[1] { data }, completeWrites: true);
365+
}).WaitAsync(TimeSpan.FromSeconds(5)));
366+
}
339367
}
340368
}

src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public async Task TwoListenersOnSamePort_DisjointAlpn_Success()
106106
QuicListenerOptions listenerOptions = CreateQuicListenerOptions();
107107
listenerOptions.ListenEndPoint = listener1.LocalEndPoint;
108108
listenerOptions.ApplicationProtocols[0] = new SslApplicationProtocol("someprotocol");
109-
listenerOptions.ConnectionOptionsCallback = (_, _, _) =>
109+
listenerOptions.ConnectionOptionsCallback = (_, _, _) =>
110110
{
111111
var options = CreateQuicServerOptions();
112112
options.ServerAuthenticationOptions.ApplicationProtocols[0] = listenerOptions.ApplicationProtocols[0];
@@ -144,5 +144,27 @@ public async Task TwoListenersOnSamePort_SameAlpn_Throws()
144144
//
145145
await AssertThrowsQuicExceptionAsync(QuicError.InternalError, async () => await CreateQuicListener(listener.LocalEndPoint));
146146
}
147+
148+
[Fact]
149+
public async Task Listener_AwaitsConnection_ListenerSurvivesGC()
150+
{
151+
TaskCompletionSource<IPEndPoint> listenerEndpointTcs = new TaskCompletionSource<IPEndPoint>();
152+
await Task.WhenAll(
153+
Task.Run(async () =>
154+
{
155+
await using var listener = await CreateQuicListener();
156+
listenerEndpointTcs.SetResult(listener.LocalEndPoint);
157+
var connection = await listener.AcceptConnectionAsync();
158+
await connection.DisposeAsync();
159+
}).WaitAsync(TimeSpan.FromSeconds(5)),
160+
Task.Run(async () =>
161+
{
162+
var endpoint = await listenerEndpointTcs.Task;
163+
await Task.Delay(TimeSpan.FromSeconds(0.5));
164+
GC.Collect();
165+
var connection = await CreateQuicConnection(endpoint);
166+
await connection.DisposeAsync();
167+
}).WaitAsync(TimeSpan.FromSeconds(5)));
168+
}
147169
}
148170
}

src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ await WhenAllOrAnyFailed(
102102
}
103103
catch (Exception ex)
104104
{
105-
_output?.WriteLine($"Failed to {ex.Message}");
105+
_output?.WriteLine($"Failed to connect: {ex.Message}");
106106
throw;
107107
}
108108
}));
@@ -153,14 +153,5 @@ public override void Dispose()
153153
}
154154
}
155155
}
156-
157-
[OuterLoop("May take several seconds")]
158-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
159-
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
160-
[ActiveIssue("https://github.com/dotnet/runtime/issues/73377")]
161-
public override Task Parallel_ReadWriteMultipleStreamsConcurrently()
162-
{
163-
return Task.CompletedTask;
164-
}
165156
}
166157
}

0 commit comments

Comments
 (0)