Skip to content

fix(drivers): restart a driver that stays offline - #1628

Open
frahlg wants to merge 2 commits into
masterfrom
cursor/driver-restart-watchdog-2017
Open

frahlg wants to merge 2 commits into
masterfrom
cursor/driver-restart-watchdog-2017

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Problem and result

A Pixii stayed offline after a power outage until someone restarted the driver by hand. Core only restarted a driver on a config change or a manual RestartByName, and a driver that failed to start at boot was logged once and never retried.

The registry now restarts a driver that has been offline or stale, or that failed to start, with backoff 30s, 1 min, 2 min, 5 min, then every 5 min. A fresh reading resets the backoff. Each automatic restart is logged, and /api/status reports auto_restarts, auto_restart_reason, auto_restart_error, and next_auto_restart_ms.

Related to #522. That GoodWe case is a dead Modbus TCP session after the controller reboots. TCP keepalive already landed; this does not change the dialer. It does replace a Modbus driver, including a Pixii or a GoodWe, whose session has gone stale, by stopping it (which runs driver_default_mode) and starting it again.

Scope and safety

Disabled drivers and observe-only drivers are left alone. A disabled entry does not stop a driver that is still running; reload owns that.

A running cloud or OAuth driver is not torn down when it goes quiet. driver_init is where a rotated refresh token is applied, and restarting a live login would repeat the re-auth loop Registry.Add warns about. A cloud driver that failed to start is retried on the same backoff.

MQTT, WebSocket, and TCP drivers that are polling and have never delivered a reading are waiting, not dead. A push driver that has delivered data and then goes stale is restarted. That memory survives the health record being wiped on remove.

The watchdog takes the same per-driver lifecycle lock as Reload and Restart, then uses the same remove-and-add path, including driver_default_mode on the way out and again at startup. It does not run inside the control tick. On process exit it is cancelled and waited for before ShutdownAll, so it cannot start a driver shutdown has already removed.

The existing control-loop watchdog still marks a driver offline and sends its default mode. This one replaces the process when that is not enough. Stale site-meter data still stops dispatch; planner output is still not sent to hardware.

Open PRs #1003, #1364, #1337, and #1546 also touch main.go or api.go, in other regions. This branch is based on master and does not include those changes.

Verification

cd go && make -C .. drivers
cd go && go vet ./...
cd go && go test -count=1 -timeout 300s ./...
cd go/protocol && go vet ./... && go test -count=1 ./...

All passed. The watchdog tests cover offline then restart then recovered (including driver_default_mode), backoff growth through 30s/1m/2m/5m/5m and reset to 30s, disabled and observe-only left alone, start failure retried, a running cloud driver left alone with start failures still retried, MQTT waiting left alone, the lifecycle lock, and a cancelled context.

/api/status covers auto_restarts, the reason, the error, and the next attempt. GitHub CI on this PR is green, including test / go test + vet.

No hardware run. A Pixii or GoodWe that stays offline after a power loss or a controller reboot still needs a site check.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

A driver that went offline or failed to start stayed down until a config
reload or a manual restart. The watchdog retries with backoff, and leaves
disabled, observe-only, waiting push, and running cloud drivers alone.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review October 14, 2026 15:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-15T05:26:30.757785Z a9dd434 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce8fffd49e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/cmd/ftw/main.go
Comment on lines +808 to +811
cfgMu.RLock()
defer cfgMu.RUnlock()
timeout := time.Duration(cfg.Site.WatchdogTimeoutS) * time.Second
return config.WithBatterySoCBounds(cfg.ModbusControlDrivers(), cfg.Batteries), timeout

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate configuration before restarting a driver

The callback releases cfgMu after returning a copied driver list, before MaintainDrivers acquires the per-driver lifecycle gate. If a failed driver's retry is already due and the operator disables or changes it during that window, configreload.Apply can install and reload the new configuration first, after which the watchdog still starts the driver using the stale snapshot. Because later watchdog passes deliberately leave a running disabled driver alone, it can remain active indefinitely despite the saved setting; recheck the current configuration after acquiring the lifecycle gate or otherwise reject stale snapshots.

AGENTS.md reference: AGENTS.md:L20-L21

Useful? React with 👍 / 👎.

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.

Fixed in a9dd434. The snapshot is still copied under cfgMu and that lock is dropped before the restart, so a settings save is not blocked for the whole reconnect.

After acquireLifecycle, and before a restart is scheduled, the watchdog reads the installed config again. It skips the restart when that driver is gone, disabled, or observe-only, or when sameDriverConfig says the snapshot is stale. Reload already owns that transition, and a later pass no longer has a driver to leave running. An unchanged config is still restarted.

TestRestartWatchdogRechecksConfigAfterLifecycleGate holds the lifecycle gate, checks that the second read has not happened yet, then releases it with a disabled, observe-only, changed, or unchanged config. CI is green.

The watchdog copied the driver list, dropped the config lock, and could
then start that copy after a reload had already disabled or edited the
driver. It now re-reads the installed config after the lifecycle gate
and leaves a disabled, observe-only, or changed driver to reload.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@cursor

cursor Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@frahlg

frahlg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref a9dd4345eb98e0c80e734d3590cde25e76941a45 does not exist
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants