Skip to content

test(node): retry websocket port allocation in separate-port test - #2900

Open
gzliudan wants to merge 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:test-node-flake-websocket-port
Open

gzliudan wants to merge 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:test-node-flake-websocket-port

Conversation

@gzliudan

@gzliudan gzliudan commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

TestWebsocketHTTPOnSeparatePort_WSRequest probed for a free port by listening on :0 and closing the socket, then handed the number to the node. The node binds its own HTTP listener on :0 first, and the kernel could return the just-released port to it — the WebSocket bind then failed with EADDRINUSE, node.Start() rolled the HTTP server back, and the test took the shard down.

 TestWebsocketHTTPOnSeparatePort_WSRequest
-  listener := net.Listen("tcp", "127.0.0.1:0")   # probe
-  listener.Close()
-  startHTTP(t, 0, port)
+  startHTTPOnSeparatePort(t)
+    freeport(t)                    # fresh probe per attempt
+    newTestNode(0, port)           # Config + New, error-returning
+    node.Start()                   # EADDRINUSE -> next attempt, 10 max
node/node_test.go
├── newTestNode                # Config + New, returns error (new)
├── createNode                 # t.Fatalf wrapper around newTestNode
├── freeport                   # probe a port, release it (new)
└── startHTTPOnSeparatePort    # probe + start + retry on EADDRINUSE (new)

The second commit only changes the failure message: the last EADDRINUSE is carried out of the loop and printed when all ten attempts are lost.

     if err := node.Start(); err != nil {
         if errors.Is(err, syscall.EADDRINUSE) {
+            lastErr = err
             continue
         }
         t.Fatalf("could not start http service on node: %v", err)
     }
     return node, port
 }
-t.Fatalf("could not start node on a free websocket port")
+t.Fatalf("could not start the node after 10 attempts: %v", lastErr)

Evidence

Before — CI run for #2837, J-N tests shard:

INFO Start http server
INFO HTTP server started  endpoint=127.0.0.1:33677
INFO HTTP server stopped  endpoint=127.0.0.1:33677
--- FAIL: TestWebsocketHTTPOnSeparatePort_WSRequest (0.00s)
    node_test.go:570: could not start http service on node: listen tcp 127.0.0.1:33677: bind: address already in use

After — same test five times, then the whole package and lint:

$ go test ./node -run TestWebsocketHTTPOnSeparatePort -count=5
ok  	github.com/XinFinOrg/XDPoSChain/node	0.034s

$ go test ./node -count=1
ok  	github.com/XinFinOrg/XDPoSChain/node	2.527s

$ go run build/ci.go lint ./node/...
0 issues.

When the ten attempts are exhausted, the failure names the endpoint and address instead of blaming the port: could not start the node after 10 attempts: listen tcp 127.0.0.1:<port>: bind: address already in use.

Merge Danger

Door: two-way — test-only, one file, no production code. A revert restores the previous behaviour, flake included.

Blast Radius: shard-only

The retry classifies a bind conflict purely as syscall.EADDRINUSE. Windows reports a WSA errno instead, so the retry is a no-op there and a lost race still fails the test; CI only runs Linux. go-ethereum carries the same test and the same race, so this is local hardening rather than a port.

Types of changes

  • test: Adding missing tests or correcting existing tests

Impacted Components

  • Not sure (Please specify below) — the node package test helper used by TestWebsocketHTTPOnSeparatePort_WSRequest

Checklist

  • N/A — test-only change: the test itself is the coverage; no devnet plan, protocol surface, backwards compatibility, or documentation to update.

TestWebsocketHTTPOnSeparatePort_WSRequest picked a "free" port by opening a
listener on port 0 and closing it again, then configured the WebSocket server
to bind that port while the HTTP server used port 0. Because the node starts
its HTTP server first, the kernel can hand the just released port straight back
to that listener, after which the WebSocket bind fails with EADDRINUSE and the
whole shard goes red. This was observed on CI in the J-N test shard.

Pick a fresh port and retry the start on EADDRINUSE instead of failing, so a
port stolen between probing and binding no longer breaks the suite. The node
closes itself when endpoint startup fails, so a discarded attempt leaks
nothing.

go-ethereum carries the same test and the same race.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 29278ca0-99b9-4e0c-9267-4b8ac5e16a4f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

When all ten attempts to start the node on a freshly probed WebSocket port
lost to a bind conflict, the helper failed with a bare "could not start node
on a free websocket port". The attempt that produced the final error had been
discarded by then, so on CI the only trace of the actual cause - which
endpoint could not bind and on what address - was already gone.

Carry the last EADDRINUSE out of the loop and print it, and say plainly that
ten attempts were made instead of implying the port was the culprit: the
retry classifies any bind conflict, not only the WebSocket one. The helper's
doc comment now also records that the classification depends on
syscall.EADDRINUSE, which Windows reports as a WSA errno instead, so the
retry is a no-op there.

Behaviour on the success path is unchanged; only the diagnosis of a lost
race changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants