Skip to content

Commit f04f22b

Browse files
authored
Bound each connect attempt in InnerSocketWaiter with a timeout (#857)
ReceiveTimeout never applied to ConnectAsync, so a refused connect could block ~2s per attempt (~200s worst case). Use a 100ms CancellationToken per attempt with a fresh socket, giving a bounded ~20s give-up. Add tests.
1 parent 8008f7f commit f04f22b

2 files changed

Lines changed: 65 additions & 5 deletions

File tree

src/Tests/SocketWaiterTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Net;
2+
using System.Net.Sockets;
3+
using VerifyTestsPlaywright;
4+
5+
public class SocketWaiterTests
6+
{
7+
[Test]
8+
public async Task Returns_when_server_already_listening()
9+
{
10+
var port = FreePort();
11+
using var listener = new TcpListener(IPAddress.Loopback, port);
12+
listener.Start();
13+
14+
await SocketWaiter.Wait(port);
15+
}
16+
17+
[Test]
18+
public async Task Retries_until_server_starts()
19+
{
20+
var port = FreePort();
21+
using var listener = new TcpListener(IPAddress.Loopback, port);
22+
23+
// start waiting before anything is listening
24+
var waitTask = SocketWaiter.Wait(port);
25+
26+
// connection is refused, so Wait should still be retrying rather than
27+
// giving up (the pre-fix code burned through every attempt in a few ms)
28+
await Task.Delay(300);
29+
Assert.That(waitTask.IsCompleted, Is.False);
30+
31+
// bring the server up; Wait should connect on a subsequent retry
32+
listener.Start();
33+
await waitTask;
34+
}
35+
36+
// Slow (~20s) by design: it exercises the full give-up path. Each attempt is
37+
// bounded to ~100ms by the connect timeout, so 100 attempts exhaust in ~20s.
38+
// Without that timeout a refused connect can block ~2s, blowing out to ~200s.
39+
[Test]
40+
public async Task Gives_up_in_bounded_time()
41+
{
42+
var port = FreePort(); // nothing ever listens here
43+
44+
var waitTask = SocketWaiter.Wait(port);
45+
46+
var finished = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(45)));
47+
Assert.That(finished, Is.SameAs(waitTask), "Wait blocked too long before giving up");
48+
49+
Assert.ThrowsAsync<TimeoutException>(() => waitTask);
50+
}
51+
52+
static int FreePort()
53+
{
54+
var listener = new TcpListener(IPAddress.Loopback, 0);
55+
listener.Start();
56+
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
57+
listener.Stop();
58+
return port;
59+
}
60+
}

src/Verify.Playwright/InnerSocketWaiter.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ static class InnerSocketWaiter
44
{
55
public static async Task Wait(int port)
66
{
7-
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
8-
socket.ReceiveTimeout = 100;
97
for (var i = 0; i < 100; i++)
108
{
9+
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
1110
try
1211
{
13-
await socket.ConnectAsync(new DnsEndPoint("localhost", port));
12+
using var cancellation = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
13+
await socket.ConnectAsync(new DnsEndPoint("localhost", port), cancellation.Token);
1414
return;
1515
}
16-
catch (SocketException)
16+
catch (Exception exception) when (exception is SocketException or OperationCanceledException)
1717
{
18-
//no op
18+
await Task.Delay(100);
1919
}
2020
}
2121

0 commit comments

Comments
 (0)