Skip to content

test(qwp): fix port collision flake in walk tracker test - #103

Merged
bluestreak01 merged 2 commits into
mainfrom
vi_fix_walk_port_race
Oct 10, 2026
Merged

bluestreak01 merged 2 commits into
mainfrom
vi_fix_walk_port_race

Conversation

@bluestreak01

Copy link
Copy Markdown
Member

Test-only change. It fixes an intermittent failure of QwpQueryClientWalkTrackerTest.testWalk_TransportFailureContinuesWalk.

Failure

The test failed on questdb/questdb#7680 CI, job "SelfHosted Other tests (B) on linux-x64-zfs" (build 274943):

java.lang.IllegalArgumentException: duplicate addr entry: localhost:46809
	at io.questdb.client.impl.ConfigView.parseEntry(ConfigView.java:327)
	at io.questdb.client.impl.ConfigView.getHostPorts(ConfigView.java:169)
	at io.questdb.client.cutlass.qwp.client.QwpQueryClient.validateConfig(QwpQueryClient.java:504)
	at io.questdb.client.cutlass.qwp.client.QwpQueryClient.fromConfig(QwpQueryClient.java:387)
	at io.questdb.client.test.cutlass.qwp.client.QwpQueryClientWalkTrackerTest.testWalk_TransportFailureContinuesWalk(QwpQueryClientWalkTrackerTest.java:451)

Root cause

The test builds addr=localhost:<portDead>,localhost:<portOk>:

  • TestPorts.findUnusedPort() supplies portDead: it binds port 0, closes the probe socket, and returns the number. Nothing holds the port afterwards.
  • new TestWebSocketServer(NOOP_HANDLER) then binds its own port 0 for portOk, and the kernel may hand it the port the probe just released.

In the failing run both values came out as 46809. ConfigView.parseEntry() correctly rejects the duplicate (host, port) pair, so fromConfig() threw before the client attempted any connection. The walk-tracker logic the test targets never ran, and the product code behaved as specified.

The TestPorts.findUnusedPorts(int) javadoc already describes the same race for two back-to-back findUnusedPort() calls. This test pairs one probe with a server's own bind(0), which that helper does not cover.

Change

The test now creates the server first and probes for portDead while the server's listener holds portOk. TestWebSocketServer binds its listener in the constructor and keeps it until close(), and the kernel never assigns a bind(0) port that a live socket on the same address holds, so the two ports cannot collide.

Tradeoff: the test keeps the suite's existing bind-close-reuse exposure, where another process could bind portDead between the probe and the client's connect attempt. Every other pre-selected-port test in the suite accepts the same exposure. The change touches no production code and no shared test helper.

Test plan

  • mvn -pl core test -Dtest=QwpQueryClientWalkTrackerTest: 12/12 green on JDK 25, both on 0b9b5766 (the commit questdb master pins) and on this branch (main plus the fix).
  • A green run cannot prove the absence of a rare random collision; the fix relies on kernel bind semantics rather than on timing.
  • Not built on JDK 8; the change only reorders existing calls and adds a comment.

testWalk_TransportFailureContinuesWalk failed in CI with "duplicate
addr entry: localhost:46809" thrown by ConfigView.parseEntry().

The test called TestPorts.findUnusedPort() to pick a refused port. That
helper binds port 0, closes the probe socket and returns the number.
The test then created a TestWebSocketServer, whose constructor binds
its own port 0. Nothing held the probe port in between, so the kernel
could hand the same port to the server. Both addr entries then pointed
at the same endpoint, and QwpQueryClient.fromConfig() rejected the
config before the walk ever ran.

The test now creates the server first and probes for the dead port
while the server's listener holds its port. The kernel never assigns a
port that a live listener owns, so the two ports always differ. The
remaining exposure - another process taking the dead port before the
client connects - matches every other pre-selected-port test in the
suite.
@bluestreak01

Copy link
Copy Markdown
Member Author

Reviewing PR #103 at level 3, the full review: surface map, discovery fan-out and independent falsification.

PR #103 — test(qwp): fix port collision flake in walk tracker test

Verdict: approve. The root cause is correct and the fix closes it. I found nothing to report at any severity.

Reviewed head b5e467ea against merge base c41a4172. The diff is one test file (+6/−1) and touches no production code and no shared helper.

Title and description: the title follows Conventional Commits and the QWP label fits. The description sets out failure, root cause, change, tradeoff and test plan, and each claim checked out. Commit 31ca2bee follows the message conventions.

Submodule provenance: no submodule pointer moved (zstd is unchanged), and the diff contains no binaries.

Is the fix real?

  • Root cause: the CI error duplicate addr entry: localhost:46809 has one throw site (ConfigView.java:327). This test can only reach it when portDead == portOk. At base nothing holds the probe's port before the server's own bind(0), so the server can be given the same port.
  • Fix: the server binds and listens in its constructor (TestWebSocketServer.java:181-183) and keeps the socket until close(). On the same loopback address, a bind(0) cannot be handed a port held by a listening socket, even though Java sets SO_REUSEADDR on these sockets.
  • Residual exposure: another process could still take portDead, or the client could connect to itself on that port. Both risks were already there at base, the window is the same or slightly shorter, and the PR states this.

Runtime evidence

I ran these on Temurin 8u504, Linux 6.6 aarch64 container.

Experiment Base order Head order
Socket-only harness, default port range, 200,000 iterations 27 collisions (≈1/7,400) 0
Same harness, port range narrowed to 64 ports, 20,000 iterations 1,227 collisions 0
The real test method, port range narrowed to 100 ports, 1,000 runs 958 passed; all 42 failures were IllegalArgumentException: duplicate addr entry: localhost:<port> 1,000/1,000 passed
  • JDK 8 build: head compiles on Corretto 8u492 (class file major version 52). The full QwpQueryClientWalkTrackerTest passes 12/12 on JDK 8.
  • The bytecode confirms the new order: server constructor, then getPort(), then findUnusedPort(), then start().

Critical

None.

Moderate

None.

Minor

None.

Coverage map

The test gate passes with 0 admitted coverage gaps. No production behaviour changed, and the test still drives the same refused-first-endpoint path with the same assertions. The change can be tested here alone, so no OSS or Enterprise tandem PR is needed (searches by branch name found none).

Summary

  • Verdict: approve. 0 Critical, 0 Moderate, 0 Minor; nothing found inside the diff or at callsites outside it.

@bluestreak01 bluestreak01 added the QUEUED FOR MERGE Approved PR in the merge queue. Do not merge master into this PR. label Oct 10, 2026
@bluestreak01
bluestreak01 merged commit 21e3268 into main Oct 10, 2026
17 checks passed
@bluestreak01
bluestreak01 deleted the vi_fix_walk_port_race branch October 10, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QUEUED FOR MERGE Approved PR in the merge queue. Do not merge master into this PR. QWP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant