Skip to content

Commit b0fa318

Browse files
committed
Fix Timer rooting issue with SocketsHttpHandler (dotnet/corefx#32793)
dotnet/corefx#32793
1 parent 0376d3c commit b0fa318

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

StandardSocketsHttpHandler.FunctionalTests/SocketsHttpHandlerTest.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Net.Security;
1010
using System.Net.Sockets;
1111
using System.Net.Test.Common;
12+
using System.Runtime.CompilerServices;
1213
using System.Security.Authentication;
1314
using System.Security.Cryptography.X509Certificates;
1415
using System.Text;
@@ -940,6 +941,48 @@ await server.AcceptConnectionAsync(async connection2 =>
940941
}
941942
}
942943
}
944+
945+
[OuterLoop]
946+
[Fact]
947+
public void HandlerDroppedWithoutDisposal_NotKeptAlive()
948+
{
949+
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
950+
HandlerDroppedWithoutDisposal_NotKeptAliveCore(tcs);
951+
for (int i = 0; i < 10; i++)
952+
{
953+
GC.Collect();
954+
GC.WaitForPendingFinalizers();
955+
}
956+
Assert.True(tcs.Task.IsCompleted);
957+
}
958+
959+
[MethodImpl(MethodImplOptions.NoInlining)]
960+
private void HandlerDroppedWithoutDisposal_NotKeptAliveCore(TaskCompletionSource<bool> setOnFinalized)
961+
{
962+
// This relies on knowing that in order for the connection pool to operate, it needs
963+
// to maintain a reference to the supplied IWebProxy. As such, we provide a proxy
964+
// that when finalized will set our event, so that we can determine the state associated
965+
// with a handler has gone away.
966+
IWebProxy p = new PassthroughProxyWithFinalizerCallback(() => setOnFinalized.TrySetResult(true));
967+
968+
// Make a bunch of requests and drop the associated HttpClient instances after making them, without disposal.
969+
Task.WaitAll((from i in Enumerable.Range(0, 10)
970+
select LoopbackServer.CreateClientAndServerAsync(
971+
url => new HttpClient(new StandardSocketsHttpHandler { Proxy = p }).GetStringAsync(url),
972+
server => server.AcceptConnectionSendResponseAndCloseAsync())).ToArray());
973+
}
974+
975+
private sealed class PassthroughProxyWithFinalizerCallback : IWebProxy
976+
{
977+
private readonly Action _callback;
978+
979+
public PassthroughProxyWithFinalizerCallback(Action callback) => _callback = callback;
980+
~PassthroughProxyWithFinalizerCallback() => _callback();
981+
982+
public ICredentials Credentials { get; set; }
983+
public Uri GetProxy(Uri destination) => destination;
984+
public bool IsBypassed(Uri host) => true;
985+
}
943986
}
944987

945988
public sealed class SocketsHttpHandler_PublicAPIBehavior_Test

StandardSocketsHttpHandler/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ public HttpConnectionPoolManager(HttpConnectionSettings settings)
8989
restoreFlow = true;
9090
}
9191

92-
_cleaningTimer = new Timer(s => ((HttpConnectionPoolManager)s).RemoveStalePools(), this, Timeout.Infinite, Timeout.Infinite);
92+
// Create the timer. Ensure the Timer has a weak reference to this manager; otherwise, it
93+
// can introduce a cycle that keeps the HttpConnectionPoolManager rooted by the Timer
94+
// implementation until the handler is Disposed (or indefinitely if it's not).
95+
_cleaningTimer = new Timer(s =>
96+
{
97+
var wr = (WeakReference<HttpConnectionPoolManager>)s;
98+
if (wr.TryGetTarget(out HttpConnectionPoolManager thisRef))
99+
{
100+
thisRef.RemoveStalePools();
101+
}
102+
}, new WeakReference<HttpConnectionPoolManager>(this), Timeout.Infinite, Timeout.Infinite);
93103
}
94104
finally
95105
{

0 commit comments

Comments
 (0)