Skip to content

EEBus: retry test server bind to survive freePort race - #31625

Closed
andig wants to merge 1 commit into
masterfrom
fix/eebus-test-freeport-bind-race
Closed

EEBus: retry test server bind to survive freePort race#31625
andig wants to merge 1 commit into
masterfrom
fix/eebus-test-freeport-bind-race

Conversation

@andig

@andig andig commented Jul 9, 2026

Copy link
Copy Markdown
Member

follows #31617

freePort() in the eebus tests probes a free port (listen :0, read port, close) and hands it to the server, which binds it later. Another listener can take the port in that gap — a TOCTOU that surfaces as bind: address already in use and flakes TestShipPairing and TestControlBoxGridGuardHeartbeat (the now-dominant remaining flake in this package, ~1/55 locally).

An ephemeral port (Port: 0) isn't an option: ship-go announces the configured port via mDNS and rejects port 0, so the port has to be known up front.

  • newServerOnFreePort retries NewServer on a fresh port when the bind loses the race; the initial server setups in both tests use it.
  • Added TestNewServerOnFreePort_RetriesPastOccupiedPort: occupies the port first and asserts the helper retries past it (fails without the retry).

🤖 Generated with Claude Code

freePort() probes a port then closes it, so another listener can take it
before the eebus server binds - a TOCTOU that surfaces as "bind: address
already in use" and flakes TestShipPairing/TestControlBoxGridGuardHeartbeat.
An ephemeral port is not an option: ship-go announces the configured port
via mDNS and rejects port 0.

Retry NewServer on a fresh port when the bind loses the race. Add a
deterministic test that occupies the port first and asserts the retry.
@andig andig added the bug Something isn't working label Jul 9, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The retry loop in newServerOnFreePort uses for range 5, which is not valid Go; consider a conventional counted loop like for i := 0; i < 5; i++ {} to ensure the code compiles and the intent is clear.
  • Overriding the global nextFreePort in TestNewServerOnFreePort_RetriesPastOccupiedPort introduces shared mutable state that could behave incorrectly with parallel tests; consider using dependency injection or a non-global hook to avoid test interference.
  • The logic in newServerOnFreePort relies on checking strings.Contains(err.Error(), "address already in use"), which is brittle; it would be more robust to detect the bind failure via a typed error or errors.Is against a well-defined sentinel error.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The retry loop in newServerOnFreePort uses `for range 5`, which is not valid Go; consider a conventional counted loop like `for i := 0; i < 5; i++ {}` to ensure the code compiles and the intent is clear.
- Overriding the global `nextFreePort` in TestNewServerOnFreePort_RetriesPastOccupiedPort introduces shared mutable state that could behave incorrectly with parallel tests; consider using dependency injection or a non-global hook to avoid test interference.
- The logic in newServerOnFreePort relies on checking `strings.Contains(err.Error(), "address already in use")`, which is brittle; it would be more robust to detect the bind failure via a typed error or `errors.Is` against a well-defined sentinel error.

## Individual Comments

### Comment 1
<location path="server/eebus/test/pairing_test.go" line_range="49" />
<code_context>
+
+		if _, err := server.Instance(); err == nil {
+			return inst
+		} else if !strings.Contains(err.Error(), "address already in use") {
+			require.NoError(t, err, "instance")
+		}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** String matching on the error message may be brittle; consider a more robust check or a test to guard against changes

`newServerOnFreePort` relies on `strings.Contains(err.Error(), "address already in use")` to detect the bind race. If the error text changes (OS differences, refactor, etc.), this will stop retrying and instead hit `require.NoError`. Prefer using `errors.Is` with a specific error type if available, or add a unit test that asserts the helper still retries when `server.Instance` returns the bind error so future changes don’t silently break this behavior.

Suggested implementation:

```golang
		if _, err := server.Instance(); err == nil {
			return inst
		}

		// if this isn't the bind-race / "address already in use" case, fail the test
		// instead of retrying on an unexpected error
		if !errors.Is(err, syscall.EADDRINUSE) {
			require.NoError(t, err, "instance")
		}

```

1. In the import block of `server/eebus/test/pairing_test.go`, add:
   - `errors`
   - `syscall`
   and remove `strings` if it is no longer used elsewhere in the file.
   For example:
   `import ("errors" "syscall" "testing" "github.com/stretchr/testify/require" ...)`
2. Add or complete a test function (e.g. `TestNewServerOnFreePort_RetriesPastOccupiedPort`) that:
   - Arranges for `server.Instance()` to fail initially with an `EADDRINUSE`-wrapped error.
   - Calls the helper (the function containing this retry loop, likely `newServerOnFreePort`).
   - Asserts that the helper ultimately returns a running server instance instead of failing.
   This test will guard against future changes that stop using `syscall.EADDRINUSE` or otherwise break the retry behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


if _, err := server.Instance(); err == nil {
return inst
} else if !strings.Contains(err.Error(), "address already in use") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): String matching on the error message may be brittle; consider a more robust check or a test to guard against changes

newServerOnFreePort relies on strings.Contains(err.Error(), "address already in use") to detect the bind race. If the error text changes (OS differences, refactor, etc.), this will stop retrying and instead hit require.NoError. Prefer using errors.Is with a specific error type if available, or add a unit test that asserts the helper still retries when server.Instance returns the bind error so future changes don’t silently break this behavior.

Suggested implementation:

		if _, err := server.Instance(); err == nil {
			return inst
		}

		// if this isn't the bind-race / "address already in use" case, fail the test
		// instead of retrying on an unexpected error
		if !errors.Is(err, syscall.EADDRINUSE) {
			require.NoError(t, err, "instance")
		}
  1. In the import block of server/eebus/test/pairing_test.go, add:
    • errors
    • syscall
      and remove strings if it is no longer used elsewhere in the file.
      For example:
      import ("errors" "syscall" "testing" "github.com/stretchr/testify/require" ...)
  2. Add or complete a test function (e.g. TestNewServerOnFreePort_RetriesPastOccupiedPort) that:
    • Arranges for server.Instance() to fail initially with an EADDRINUSE-wrapped error.
    • Calls the helper (the function containing this retry loop, likely newServerOnFreePort).
    • Asserts that the helper ultimately returns a running server instance instead of failing.
      This test will guard against future changes that stop using syscall.EADDRINUSE or otherwise break the retry behavior.

@andig andig closed this Jul 9, 2026
@andig andig reopened this Jul 9, 2026
@andig andig closed this Jul 9, 2026
@andig andig reopened this Jul 9, 2026
@andig
andig marked this pull request as draft July 9, 2026 14:26
@andig andig closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant