Skip to content

Fix flaky TestShipPairing: patched ship-go fork for double-connection race#31493

Merged
andig merged 2 commits into
masterfrom
fix/ship-go-double-connect-flake
Jul 5, 2026
Merged

Fix flaky TestShipPairing: patched ship-go fork for double-connection race#31493
andig merged 2 commits into
masterfrom
fix/ship-go-double-connect-flake

Conversation

@andig

@andig andig commented Jul 5, 2026

Copy link
Copy Markdown
Member

fixes flaky CI failures in TestShipPairing, e.g. https://github.com/evcc-io/evcc/actions/runs/28740106700. Should fix #31465.

CI intermittently fails with "paired device not routed to consumer" after the test's 1-minute connect timeout. Traced to two compounding bugs in enbility/ship-go's connection handling, reproduced locally by stress-running the test under a noisier network than CI (100% failure rate before either fix, 10/10 passes after both, with the original timeouts left untouched):

Both are fixed in evcc-io/ship-go (fork, branch fix/double-connection-outgoing-dedup, based on our exact currently-pinned commit): the first is enbility/ship-go#85's fix cherry-picked as-is, the second is enbility/ship-go#86 alongside it. This PR only changes go.mod/go.sum to point at that fork via a replace directive, mirroring the existing eebus-go fork pattern already in this repo.

No timeouts were changed — verified the fix holds under the original 1-minute per-connect timeouts.

🤖 Generated with Claude Code

CI intermittently fails TestShipPairing with 'paired device not routed
to consumer' after a 1-minute timeout (e.g. run 28740106700). Root
cause traced to two bugs in enbility/ship-go's connection handling that
compound under concurrent bidirectional pairing:

- Two concurrent outgoing dials to the same SKI can both pass the
  isSkiConnected check before either registers, establish, and then
  tear each other down (enbility/ship-go#85, open upstream, unmerged).
- sendSpineData only tore down the connection when the writer already
  reported it closed; a write failing for any other reason (e.g. the
  remote closed it first) left a zombie connection that silently
  failed every future send and never freed the SKI for reconnect.

Both are fixed in evcc-io/ship-go (branch
fix/double-connection-outgoing-dedup, cherry-picked onto our exact
pinned commit): the first is PR #85 as-is, the second is a new fix
alongside it. 10/10 stress runs pass locally with these fixes and the
original 1-minute timeouts unchanged; the same run reliably failed
before either fix.

@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 left some high level feedback:

  • Consider adding a short inline comment next to the new ship-go replace directive in go.mod summarizing that it fixes the double-connection race / connection teardown bug, so the rationale for the fork is obvious without having to dig up this PR.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider adding a short inline comment next to the new `ship-go` replace directive in `go.mod` summarizing that it fixes the double-connection race / connection teardown bug, so the rationale for the fork is obvious without having to dig up this PR.

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.

@andig andig enabled auto-merge (squash) July 5, 2026 14:40
@andig andig merged commit 0b3b0a6 into master Jul 5, 2026
21 checks passed
@andig andig deleted the fix/ship-go-double-connect-flake branch July 5, 2026 14:43
uhl added a commit to enbility/ship-go that referenced this pull request Jul 6, 2026
Fix evcc-io/evcc#31465

Found while chasing an intermittent double-connection related test hang
in evcc (evcc-io/evcc#31493) that reproduces on top of #85's dedup fix.

`sendSpineData` only calls `CloseConnection` when
`IsDataConnectionClosed()` already reports the connection closed:

```go
if isClosed, err := c.dataWriter.IsDataConnectionClosed(); isClosed {
    c.CloseConnection(false, 0, "")
    return err
}
...
err = c.dataWriter.WriteMessageToWebsocketConnection(shipMsg)
if err != nil {
    logging.Log().Debug("error sending message: ", err)
    return err
}
```

If the actual websocket write fails for any other reason (e.g. the
remote already closed the connection before this side's writer noticed),
the error is logged and swallowed, but the connection is never torn
down. The `ShipConnection` stays registered as the current writer for
that SKI, so every subsequent send silently fails forever and the SKI is
never freed for a fresh connection attempt.

This surfaced concretely while stress-testing a double-connection race
scenario: after one side's connection was implicitly invalidated, the
other kept retrying sends against it every couple of seconds
indefinitely ("Error sending spine message: connection closed for remote
SKI ...", repeating), and no reconnect was ever attempted.

Fix: also call `CloseConnection(false, 0, "")` when the write itself
errors, mirroring the existing `IsDataConnectionClosed` branch. Updated
`TestSendSpineData_WriteError` to expect the resulting
`HandleConnectionClosed` call.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

Error on adding Vaillant aroTHERM plus OHPCF

1 participant