Repository navigation
fix(control): resend the default when an excluded driver returns from an outage - #1909
Merged
Merged
Conversation
… an outage A driver excluded for refusing commands while its device was unreachable waited the full 5 min retry window after the device came back, and never got its default again. The inverter kept its last forced battery command for that time. Mark such an exclusion as an outage: reads were failing at exclusion, or the watchdog took the driver offline while it was out. On the first good reading, resend driver_default_mode and let one command through. A device that reads fine and refuses still waits 5 min. 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. |
A command can reach a dead link before the next poll does. The last read had then succeeded, so the exclusion did not count as an outage, and an outage shorter than the 60 s watchdog never marked it. The driver waited the full 5 min and a Sungrow kept its forced command (rig seed 207536, 29 s outage, 187 / 3000 W held after the return). Store LastSuccess at the exclusion. A failed read with no good read since then is the same outage. A device that has read fine since it refused still keeps the full window. The changeset now says what the return does and does not fix on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed read right after an exclusion counted as an outage. A device that refuses for real on a flaky link then got a default and a trial command on every good read after a failed one: 75 trials and 150 defaults in 5 min in the new test, instead of one trial per retry window. Once a driver has had its trial and refused it with good reads, a failed read no longer marks an outage. Reads failing at the exclusion and the watchdog taking it offline still do. An accepted trial clears the mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retried flag only cleared when the excluded action was accepted. A driver back from an outage that dispatch then left idle kept it with no end, so its next short dead-link outage was not counted as one and the inverter held its last command for the full 5 min window (207535, 207536). Store when the trial began and let the guard lapse after driverRefusalRetryInterval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 27, 2026
A hybrid inverter's curtail streak can keep the refusal record alive after its battery trial was accepted. The new test checks that a dead-link outage a minute later still gets the default and a trial on return. Removing the retriedAt reset makes it fail; no other test catches that. 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.
Problem
Triage item C1, Core half. Nightly random rig run on master 83c2b24, 27 Oct 2026.
An inverter outage makes the Sungrow refuse every write. The registry cannot confirm
driver_default_mode, so the driver returnsErrControlBlockedand Core excludes it (driver refused control … driver_default_mode returned false). When the device came back, Core:defaultedlatch was still set;driverRefusalRetryInterval(5 min) had passed.During that time the inverter kept its last forced command, and the fuse guard could not use the battery. This happened in 8 of 10 seeds (207532 to 207539). The logs show
driver exclusion retry window elapsedexactly 5 min after the refusal. The refusal and the first failed poll (modbus reconnect backoff active) come within about 1 s of each other, in either order: in 207533 the refusal came at 15:01:07.115 and the first failed poll at 07.381. Forced command still running after the device came back (house elapsed seconds):In 207534 the battery discharged 5000 W from 5.6 % to 5.0 % while FTW was not dispatching it. In 207537 polls failed from 15:15:00 to 15:15:50, and the driver stayed excluded until 15:20:01.
Change
go/cmd/ftw/driver_failure_default.go:defaultedentry and returns the driver, sodriver_default_modegoes out on that tick;driver_default_modefails again, the registry's own recovery timer (100 ms doubling to 30 s) keeps retrying it.What this does not fix
Taking the driver back into dispatch only opens the gate. Nothing goes to the device unless dispatch computes a target for it. On a Sungrow the resent default does nothing yet: the driver reports the default as held without writing it once
EMS_WRITE_ATTEMPTSis reached. So the old forced command runs until FTW's next target. Rig replay of 207535 on this branch: after the return,status.dispatchstayed[]while grid was about -1 W, and the inverter held 170/2630 W for 67 s (233 s on master). The rig's R8 check will still fail on that seed until the device-drivers half lands.Rig replay of this branch before the second commit (seeds 207535 to 207537, driver pin 57984984a9bf), seconds the inverter ran a forced command while up and not dispatched:
The second commit targets the last row. The rig has not been rerun on it.
Tests
In
go/cmd/ftw/driver_failure_default_test.go:TestDriverExcludedDuringOutageGetsDefaultWhenItReadsAgain: a short outage (reads failing, no watchdog) and a long one (watchdog offline). Three battery commands are refused while offline. The test expects a default request and re-admission on the first fresh tick, and no second default after that.TestDriverRefusingAfterOutageKeepsRetryWindowWhileOnline: a refusal during a second dropout is retried on the next return. A refusal while reads are fine keeps the 5 min window, even with one failed read in between.TestDriverExcludedJustBeforeShortOutageGetsDefaultWhenItReadsAgain: the command is refused while the last read was good, then reads fail for 30 s, under the watchdog. Covers both theErrControlBlockedpath and three plain refusals. Expects a default and re-admission on the first good read.TestDriverRefusingOnFlakyLinkGetsOneEarlyTrial(third commit): a device that refuses with good reads while every other read fails. Over 5 min it gets one early trial and two defaults. On the second commit it got 75 trials and 150 defaults.TestSecondDeadLinkOutageAfterReturnGetsDefault(fourth commit): outage, return, then a gap, then a 29 s dead-link outage (207536 shape). With dispatch idle for 6 min, or a battery command accepted in the gap, the driver gets its default and comes back on the first good read. With dispatch idle for only 1 min it still waits out the window; that case pins the trade-off below.TestAcceptedTrialEndsGuardWhileOtherActionStreakRemains(test-only commit): on a hybrid inverter a refused curtail keeps the refusal record alive after the battery trial is accepted. A 29 s dead-link outage one minute later still gets its default and comes back on the first good read. It checks that an accepted trial ends the guard; with the reset line removed it fails (got [], want [sungrow]) and no other test catches it.The first two fail on master (
got [], want [sungrow]); the third fails on the first commit the same way. The fourth fails on the second commit. The fifth fails on the third commit (got [], want [sungrow], 6 min case). All pass now. The reviewer'sTestReviewSecondDeadLinkOutageAfterReturnpasses its 6 min and accepted cases; its 1 min case expects a return and fails by design (see Risk). The reviewer's edge tests (no release while reads fail or offline, no default storm when a returned driver is still blocked, Modbus exception with good reads keeps the full window) also pass.make verifyandgo test -race ./cmd/ftwpass. No real devices were touched.Risk
Low. The change only affects drivers excluded while their reads were failing or offline, or whose next read after the exclusion failed. For those, the default can go out one extra time, and they rejoin dispatch sooner. A device that keeps dropping out gets one early trial each time it comes back, at most one per 5 min. A second dead-link outage that starts within 5 min of an early trial, before the excluded action was accepted, still waits out the 5 min window, as on master; the owner should decide whether that is acceptable. A device that refuses with good reads and whose very next read happens to fail gets one early trial; if it refuses that trial with good reads, the 5 min window applies, even if later reads fail. Reads failing at the exclusion or the watchdog taking it offline still count as an outage after a trial.
Stack and merge order
Only open PR touching
driver_failure_default.go. Independent of #1911 (C9); they can merge in either order. Branch is up to date with master (7840c33).🤖 Generated with Claude Code