Repository navigation
fix(ocpp): close a socket still opening at stop, and stop test chargers safely - #1836
Merged
Merged
Conversation
…rs safely The library closes the sockets in its table when the server stops, but a handshake past the connection check joins that table only after its 101 is written. Stop now refuses new handshakes and waits, at most 2 s, for those in flight, so the close reaches them too. Test chargers dial through chargerSocket, which closes three races in ocpp-go v0.19.0's client: Write against its own Stop, the disconnect handler swap in ocppj.Client.Stop, and the Stop that waits forever when the old handler already ran. One helper starts every test server and tries fresh ports when one is lost before the library binds it. The identity-probe tests check the rate limit's lower bound and the pending timer instead of wall-clock windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Review found that a 50 ms stall before the reconnect let the second probe go on the first connection, so a fifth request arrived and the test failed with "retried beyond limit". The first request now waits an hour for its answer; the 5 ms timeout applies only after the reconnect. The test passes with a 200 ms stall forced at that point. Also test that chargerSocket hands a missed disconnect to Stop's handler once, and say in its comment that a Write during a server-side close can still race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 25, 2026
frahlg
added a commit
that referenced
this pull request
Oct 25, 2026
) * test(ocpp): every simulated charger dials through one safe socket #1836 wrapped ocpp-go's client for internal/ocpp's tests, because it races with itself on close. The OCPP simulator and two cmd/ftw tests still dialed with the plain client. The wrapper moves to its own package, internal/ocpp/chargersocket, with constructors for 1.6 and 2.0.1, and all of them use it. A test fails if any Go file in the module builds a charger on the plain client again. Core is the OCPP server and never dials, so its binary does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ocpp): the charger guard knows ocpp-go's package names Review notes: an import without an alias is named ocpp16 or ocpp2, not after its path, so the guard missed it; it missed a dot import too. It now uses the real names and checks dot imports. It skips what the go tool skips (_ and . directories, testdata, vendor) and the nested modules under go/, and leaves a file that does not parse to the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
internal/ocppis the package that fails CI most often on unrelated PRs. This PR fixes four causes. One is in Core; three are in the tests.What failed
Run 40178374052 (PR #1827), attempt 1:
The same run, attempt 2: the package hit the 300 s timeout. The dump shows the test goroutine parked for 4 minutes in
ocppj.(*Client).Stopatclient.go:169(<-cleanupC), called from the cleanup inconnectCapabilityCP.Runs 39786224865 and 39135453604, race job:
Run 39736794360, race job,
TestFailedAndDuplicateHandshakesPreserveCurrentSocket:Same message as the first: runs 39371191559 and 39557352450.
Causes
Server.Stop. ocpp-go closes the sockets in its table when the server stops (stopConnections, run fromhttp.Server.Shutdown). A handshake that has passed our connection check joins that table only after the library writes its 101. If Stop sweeps the table in that gap, the socket stays open on the stopped server. The CI log shows it:OCPP charger connected as pending ... garage-impostorafterGetConfiguration send failed ... ocppj server is not started. Core starts and stops the server only once per process, so a running box is not affected; the fix is still in Core, because Stop should close every socket.Server.Stopnow refuses new handshakes and waits, at most 2 s, until each one already past the check has joined the table or ended. Patch changeset.ocppj.Client.Stopswaps the disconnect handler while the write pump may be reading it after a server-side close (the race above). If the pump has already called the old handler, Stop waits for a call that never comes (the 300 s timeout).ws.Client.Writechecks the connection and then queues without its lock, so a reply to Core's probe sent while the charger stops can hit the closed queue (Flaky api and ocpp tests fail CI on unrelated PRs #1712). Test chargers now dial throughchargerSocket(incharger_socket_test.go). It serializes Write with Stop, keeps the disconnect handler itself, and passes a disconnect that has already been reported to the handler Stop installs.stopAfterServerand the per-file stop orders are gone: every charger is stopped by its own cleanup, before the server.freePortand the library's bind.freePortreleases the port, and another socket can take it before ocpp-go listens. The library then exits that listener without an error, and the old helpers waited 2 s and failed withserver did not bind on port …. I saw it locally under load.startOCPPstarts every test server, sees the early exit of the listener goroutine and tries fresh ports, up to 5 times.TestIdentityProbeTimeoutAndReconnectAreBoundedfailed 3 times in 60 loaded runs withreconnect bypassed rate limit: the probe is allowed 30 ms after the first, and the test assumed it would reach the reconnect within 20 ms. It now checks only the lower bound (request k goes at least k intervals after the first connect), which load cannot break. Its stop step andTestIdentityProbeRevocationCancelsPendingRequestcheck the pending timer with a one-hour grace instead of racing a 1 or 20 ms timer.Evidence
Two throwaway tests aimed at the windows (not committed), race detector, on this laptop:
chargerSocket)Whole package on this branch, race detector:
-count=34,-cpu=1(204 runs): all pass. The same load on master, three batches of 60: 7 failed (4 on a lost port, 3 onreconnect bypassed rate limit).-count=20 -cpu=1,2,4(60 runs): all pass.New tests:
TestStopWaitsForAHandshakePastTheCheck,TestStopDoesNotWaitForAConnectedCharger, and a check at the end ofTestFailedAndDuplicateHandshakesPreserveCurrentSocketthat a failed or refused handshake does not hold up a stop.go vet ./...passes. Fullgo test ./internal/... ./cmd/ftw/...passes. CI is green on both commits, including the race job.Review
An independent review (Sonnet) found no bug in Core: every admitted handshake is counted down exactly once, on success, rejection, failed upgrade and duplicate id, and the lock order is consistent. It found one remaining timing hole in
TestIdentityProbeTimeoutAndReconnectAreBounded: a stall of one interval before the reconnect let a second probe go on the first connection. The second commit fixes it, because the first request now waits an hour for its answer, so no timer can fire before the reconnect. The test passes with a 200 ms stall forced at that point. The commit also adds a test forchargerSocket's missed-disconnect path and says in its comment that a Write while the server closes can still race.Not fixed here
StopConnectionsends on the socket's close channel after it drops its lock, so a charger that disconnects at that moment can panic the server. Core calls it when it refuses a second socket for a charger id. Library bug, not seen in CI; worth an upstream report.Startreturns success. Real, but separate from these flakes.cmd/ftwandcmd/sim-ocppalso use the ocpp-go client and could meet the same client races. Neither has failed CI from them; this PR keeps tointernal/ocpp.🤖 Generated with Claude Code