Repository navigation
fix(ocpp): a port Core cannot open fails Start and shows on each charger - #1843
Merged
Merged
Conversation
ws.Server.Start opens its own socket in a goroutine and drops a listen error into a logger Core does not read, so a taken port left the OCPP server not listening while Start reported success. Start now opens and closes each enabled port first, loads the TLS pair that ServeTLS would load later, and on any failure records the error on every charger the site names. 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 note: failing all of Start for a taken 2.0.1 port also stopped the 1.6 listener that came up before, leaving 1.6 chargers unsteered. Start now fails only for the 1.6 port. A taken 2.0.1 port is logged, that listener is skipped, and the reason goes on each named charger, which its first 1.6 message clears. The Start comment no longer says it returns once the listener is up, and startOCPP retries when a 2.0.1 listener it asked for did not start. 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.
Found on 25 October 2026 while fixing flaky OCPP tests (#1836, "Not fixed here").
Problem
If Core cannot open its OCPP port, the OCPP server never listens,
ocpp.Startstill returns success, andmain.gologsocpp: central system started. The site's chargers never connect, and nothing says why.Cause
ws.Server.Start(ocpp-go v0.19.0) opens its own socket inside the goroutine Core starts it in. Whennet.Listenfails, it hands the error to the library's logger, which Core never sets, and returns. Core's goroutine then exits without a word.ServeTLShas the same gap one step later: it loads the certificate and key inside that goroutine.serverTLSonly checked that both files exist, so a file that is present but does not hold a usable pair stopped the listener the same silent way.Fix
Startopens and closes each enabled port on every interface, as the library does, before it builds anything. A taken 1.6 port failsStartwithocpp: listen tcp :8887: bind: address already in use. A taken 2.0.1 port is logged at error level and only that listener is skipped: the 1.6 listener still runs, so the chargers that dial it are still steered.serverTLSloads the pair withtls.LoadX509KeyPairinstead of twoos.Statchecks.Startfails, or skips the 2.0.1 listener, it records the error as the last error of each charger the site names. That charger has no health row until it connects, so before this it did not appear in the device table at all. Now the help report's Devices table and/api/statuslist it, never read, with the reason beside it. A charger's first message on the 1.6 listener clears it. After the watchdog's 60 s such a charger goes offline like any OCPP charger that stops talking: the watchdog logs it, aDriverLostevent may send a notification, and/api/healthreads degraded. That is the same path a connected charger takes when it falls silent.main.goalready logsocpp: central system failed to startat error level whenStartfails. The log ring is installed before that, so the help report's log section now shows the line. No change tomain.go; itscentral system startedline still prints the configuredport_v201when that listener was skipped, right after the error line that says so.A port taken in the microseconds between the check and the library's bind still stops the listener silently. I left that window alone: closing it means reading the library's error channel, and its
Stopcloses that channel while other goroutines may still send on it.The test helper
startOCPPnow also retries whenStartitself reports a lost port, or starts without a 2.0.1 listener it asked for, sincefreePortcan lose its port to another test before the check.Evidence
New tests, run on master first:
On master the last one fails because the 2.0.1 listener's goroutine gives up on the port and
startOCPPnever sees it listen. On this branch all three pass.TestStartFailsWhenItsPortIsTakenchecks that the error names the port and that the charger's health carries it.TestATaken201PortLeavesThe16ListenerRunningchecks that a 1.6 charger still connects and clears its error, while a charger that has not connected keeps it. (The first commit had one table test for both ports; the review changed the 2.0.1 behaviour, below.)go test -race -count=2 ./internal/ocpp/passes.FTW_E2E=1 go test -race -count=2 -run 'Evify|OCPP' ./test/e2e/passes (withoutFTW_E2E=1these tests skip).go vet ./...passes.go test ./internal/... ./cmd/ftw/...passes.Not changed
Writing the master test showed a second, older race: calling
Server.Stopbefore the listener goroutine has calledcs.Startpanics withclose of nil channelin ocpp-go's dispatcher (DefaultServerDispatcher.Stop). Core reaches it only if its context ends within microseconds ofStart. It is separate from this fix.Review
An independent review (Sonnet) checked the probe against the library (the same
net.Listen("tcp", ":N"), the same IPv4/IPv6 andSO_REUSEADDRbehaviour on Linux and macOS, no TIME_WAIT problem across a re-exec), thatLoadX509KeyPairrejects no configServeTLSwould have served, and that every reader of the new health row (loadpoint controller,SumOnlineEVW, manual charging) is gated on a reading or a connected sample, so dispatch does not change. Removing the port check, the health write or the key-pair load each broke the matching test.Its findings, fixed in the second commit:
Start, so the 1.6 listener that used to come up did not, and 1.6 chargers lost their steering. A 2.0.1 port now skips only its own listener.Startcomment still said it returns "once the listener is up". The TLS test message "serving plaintext" did not fit the new case.freePortwithout a retry. It now goes throughstartOCPP.It also noted, and I left alone, that the offline transition after 60 s is new behaviour for a charger that never connected (described above), and that a charger id equal to a Lua driver name would share its health row, which was already true for its messages.
Patch changeset.
🤖 Generated with Claude Code