Skip to content

Commit b40e782

Browse files
committed
test(qwp): allocate distinct ports atomically in the all-unreachable walk test
CI (linux-x64, build 247632) failed QwpQueryClientWalkTrackerTest. testWalk_AllUnreachableThrowsHttpClientException with IllegalArgumentException: "duplicate addr entry: localhost:39695". Root cause: the test picked its two unreachable ports with two back-to-back TestPorts.findUnusedPort() calls. That helper is bind-close-return -- once its probe ServerSocket closes, the port goes back to the kernel's ephemeral pool, and Linux readily hands the just-released port to the next bind(0). Both calls returned the same port, the config became addr=localhost:P,localhost:P, and ConfigView's duplicate-addr validation (84da57d, pre-existing on main) rejected it before the endpoint walk under test ever ran. Latent flake since the test was written; unrelated to this PR's changes. Fix: TestPorts.findUnusedPorts(n) holds all n probe sockets open simultaneously, forcing the kernel to issue n distinct ports, and closes them together. The walk test uses findUnusedPorts(2). This is the only call site combining independently-picked ports into one config (verified: other multi-call tests use one port per config).
1 parent 766832d commit b40e782

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/QwpQueryClientWalkTrackerTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,14 @@ public void testWalk_AllUnreachableThrowsHttpClientException() {
170170
// The exception type is HttpClientException (transport-only
171171
// failure mode) -- distinct from QwpRoleMismatchException which
172172
// would falsely suggest a topology issue.
173-
int port1 = TestPorts.findUnusedPort();
174-
int port2 = TestPorts.findUnusedPort();
173+
// findUnusedPorts (plural) holds both probe sockets open at once so
174+
// the two ports are guaranteed distinct — two separate
175+
// findUnusedPort() calls can return the SAME port (bind-close-return
176+
// lets the kernel recycle it immediately), which fails the config's
177+
// duplicate-addr validation before the walk under test even runs.
178+
int[] ports = TestPorts.findUnusedPorts(2);
175179
try (QwpQueryClient client = QwpQueryClient.fromConfig(
176-
"ws::addr=localhost:" + port1 + ",localhost:" + port2 + ";auth_timeout_ms=300;")) {
180+
"ws::addr=localhost:" + ports[0] + ",localhost:" + ports[1] + ";auth_timeout_ms=300;")) {
177181
try {
178182
client.connect();
179183
Assert.fail("expected HttpClientException on unreachable hosts");

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/TestPorts.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,44 @@ public static int findUnusedPort() {
4040
throw new RuntimeException("failed to allocate an ephemeral port", e);
4141
}
4242
}
43+
44+
/**
45+
* Allocates {@code n} DISTINCT ephemeral ports. All {@code n} probe
46+
* sockets are held open simultaneously, so the kernel is forced to hand
47+
* out {@code n} different ports; they are closed together only after
48+
* every port has been collected.
49+
* <p>
50+
* Do NOT emulate this with repeated {@link #findUnusedPort()} calls:
51+
* that helper is bind-close-return, and once its probe socket closes the
52+
* port returns to the kernel's ephemeral pool — Linux readily hands the
53+
* just-released port straight back to the next {@code bind(0)}, so two
54+
* back-to-back calls can return the SAME port. That exact race made a
55+
* multi-addr config fail validation with "duplicate addr entry" in CI.
56+
*/
57+
public static int[] findUnusedPorts(int n) {
58+
if (n <= 0) {
59+
throw new IllegalArgumentException("n must be > 0: " + n);
60+
}
61+
ServerSocket[] sockets = new ServerSocket[n];
62+
int[] ports = new int[n];
63+
try {
64+
for (int i = 0; i < n; i++) {
65+
sockets[i] = new ServerSocket(0, 50, InetAddress.getLoopbackAddress());
66+
ports[i] = sockets[i].getLocalPort();
67+
}
68+
return ports;
69+
} catch (IOException e) {
70+
throw new RuntimeException("failed to allocate " + n + " ephemeral ports", e);
71+
} finally {
72+
for (ServerSocket s : sockets) {
73+
if (s != null) {
74+
try {
75+
s.close();
76+
} catch (IOException ignored) {
77+
// best-effort; the probe socket carries no state
78+
}
79+
}
80+
}
81+
}
82+
}
4383
}

0 commit comments

Comments
 (0)