fix(server): Restore configs truncated by a full disk or a reboot - #7369
fix(server): Restore configs truncated by a full disk or a reboot#7369balamurali27 wants to merge 8 commits into
Conversation
Confidence Score: 4/5The PR is not yet safe to merge because recovery can still execute on a pre-reboot host. Ignoring the shutdown timeout allows the next connection check to succeed against a machine whose asynchronous reboot has not started, so restoration can finish before the reboot it is intended to follow. Files Needing Attention: press/playbooks/roles/restore_truncated_configs/tasks/main.yml Prompt To Fix All With AI### Issue 1
press/playbooks/roles/restore_truncated_configs/tasks/main.yml:12
**Recovery precedes delayed reboot**
If an asynchronous provider reboot takes more than 300 seconds to close SSH, `failed_when: false` ignores the timeout and `wait_for_connection` accepts the still-running pre-reboot host, causing recovery to report success before the reboot and leaving subsequently truncated configuration unrepaired.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (7): Last reviewed commit: "fix(server): Do not fail the restore whe..." | Re-trigger Greptile |
The agent renames a config file to <name>.bak before it writes the new one. A full disk, or a machine that goes away between the rename and the write, leaves config.json, common_site_config.json or site_config.json empty, half-written, or missing. The .bak is then the last good copy. The play restores a config only when the live file does not parse as JSON and the backup does. `mode: preserve` keeps the mode of the backup, so a config that the failed write removed does not come back world readable. Agent write path: https://github.com/frappe/agent/blob/master/agent/base.py (`set_config`) and `agent/bench.py` (`set_bench_config`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
…increase These operations stop the machine, or run while the disk is full. That is when a config write in flight gets truncated, and the site or the bench comes back with an unreadable config.json. Each press job now runs the restore play as the last task. Increase Disk Size runs it before the bench restart, so the benches come up with a good config. The two reboot methods enqueue it, because neither goes through a press job. Put the task in the press jobs, not in `extend_ec2_volume`, so the OCI disk path is covered by the same code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
4377a29 to
087580d
Compare
Codecov Report❌ Patch coverage is ❌ Your patch status has failed because the patch coverage (74.07%) is below the target coverage (75.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## develop #7369 +/- ##
===========================================
+ Coverage 60.85% 61.04% +0.19%
===========================================
Files 1056 1059 +3
Lines 99682 100099 +417
Branches 1632 1643 +11
===========================================
+ Hits 60658 61103 +445
+ Misses 38986 38955 -31
- Partials 38 41 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Tick the box to add this pull request to the merge queue (same as
|
Two problems from review. The play swallowed every failure, so a press job reported success and went on to restart the benches on top of a truncated config. The play now verifies every config at the end, and the method throws when the play fails. The operator retries with the button. `reboot` on AWS returns while the machine is still up, so `wait_for_connection` succeeded before the shutdown and the restore ran on the pre-reboot machine. The two reboot methods now pass `wait_for_reboot`, which makes the play wait for port 22 to close first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
The verification scanned the whole tree again and checked every config on the server. A config that was already broken before the play, on an unrelated bench, would fail a job that had nothing to do with it. Check the ones the play found unreadable instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
`on_press_job_failure` reverted the Plan Change whenever any task failed. The tasks after `resize_virtual_machine` — `set_additional_config`, `increase_disk_size`, and now `restore_truncated_configs` — run on a machine that is already on the new type. Reverting there bills the team for a size the machine no longer has, and leaves the record and the provider out of step. Sync the machine and skip the revert when it already carries the new type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
…e portal one The desk Reboot button sits on Virtual Machine, so an operator rebooting from desk got no config restore. Move the hook to `VirtualMachine.reboot`, which every reboot path goes through, and drop the duplicate from `Server.reboot`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
Reverts 248a528. `VirtualMachine.reboot` is the low-level provider call, and every caller does not want a config restore behind it. Keep the hook on `Server.reboot`, where a reboot is a user-facing server action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
The reboot hook runs on the long queue. When that queue is busy the play can start after the machine is already back, SSH never goes down, and the `state: stopped` wait timed out and failed the play. The truncated config was then left alone, which is the one thing the play exists to prevent. The wait is now best effort. The cost is up to five minutes of waiting on a late start, on a queue where that does not matter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6
| timeout: 300 | ||
| delegate_to: localhost | ||
| become: no | ||
| failed_when: false |
There was a problem hiding this comment.
Recovery precedes delayed reboot
If an asynchronous provider reboot takes more than 300 seconds to close SSH, failed_when: false ignores the timeout and wait_for_connection accepts the still-running pre-reboot host, causing recovery to report success before the reboot and leaving subsequently truncated configuration unrepaired.
Prompt To Fix With AI
This is a comment left during a code review.
Path: press/playbooks/roles/restore_truncated_configs/tasks/main.yml
Line: 12
Comment:
**Recovery precedes delayed reboot**
If an asynchronous provider reboot takes more than 300 seconds to close SSH, `failed_when: false` ignores the timeout and `wait_for_connection` accepts the still-running pre-reboot host, causing recovery to report success before the reboot and leaving subsequently truncated configuration unrepaired.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Problem
During a disk-full incident, or a sudden disk resize,
config.json,common_site_config.jsonandsite_config.jsoncome back truncated. The site or the bench then fails to start, and we restore the file by hand.The agent writes a config in two steps. It renames the live file to
<name>.bak, then copies the new content into place (agent/base.pyset_config,agent/bench.pyset_bench_config). If a job is writing a config at the moment the disk fills up or the machine goes away, the live file is left empty, half-written, or missing. The.bakis then the last good copy, sitting right next to it.What this does
A new playbook,
restore_truncated_configs.yml. It finds the.bakfiles, checks the live file and the backup withpython3 -m json.tool, and copies the backup over only the configs that do not parse.mode: preservekeeps the mode of the backup, so a config that the failed write removed does not come back world readable.The play then verifies the configs it found unreadable and fails if one of them still does not parse.
BaseServer.restore_truncated_configs()throws on a failed play, so a press job stops instead of restarting the benches on top of a broken config. The operator retries with the button.Where it runs
@taskat the end of the flow@taskbefore the bench restart, so the benches come up with a good config@taskat the end of the flowServer.reboot()wait_for_rebootServer.reboot_with_serial_console()wait_for_rebootThe two reboot methods enqueue the play because neither goes through a press job. The hook stays on
Server, where a reboot is a user-facing server action, and not onVirtualMachine.reboot, which is the low-level provider call.rebooton AWS returns while the machine is still up, sowait_for_rebootmakes the play wait for port 22 to close before it waits for the machine to come back. Without it the restore runs on the pre-reboot machine and the truncation survives. That wait is best effort: a busy long queue can start the play after the machine is already back, and a timeout there must not stop the restore.A failed restore no longer reverts a resize that worked.
ResizeServerJob.on_press_job_failureused to revert the Plan Change whenever any task failed, including the tasks that run after the machine is already on the new type. It now syncs the machine and keeps the plan when the resize itself succeeded.The task sits in the press jobs, not in
extend_ec2_volume, so the OCI disk path is covered by the same code.Known limit
The
.bakis the version before the last write, not the current config. A restore can therefore give back a config that is one write behind. That is still much better than an empty file, and Press pushes the current config again with Update Bench Configuration. Worth a follow-up if it turns out to matter.Tested on a local Frappe Cloud
Broke three real configs on an app server, each in a different way:
benches/<bench>/config.json— cut to the first 40 bytes, the way a disk-full write leaves itbenches/<bench>/sites/common_site_config.json— truncated to zero bytesbenches/<bench>/sites/<site>/site_config.json— renamed to.bakwith no live file, the way the agent leaves it when it dies between the rename and the copyRan the real press job (Increase Disk Size on that server):
All three configs parsed as JSON afterwards, and the site config was byte-identical to the copy taken before the test.
Also checked:
Failed to restore truncated configs on server: <name>. The benches are not restarted.systemctl reboot, and started the play withwait_for_reboot. The play waited out the shutdown and restored afterwards (uptime -preportedup 0 minutes).wait_for_rebootagainst a machine that was already up and would not reboot. The down-wait timed out after five minutes, and the play went on to restore the config.Considered and left out
Snapshot DiskandUpgrade MariaDBstop docker or mariadb, not the agent that writes the configs. The machine stays up.Increase SwapandReset Swapwrite a 4 GB swap file, which can push the disk to full. The risk is indirect and the job restarts nothing, so it is left out. The same one-line task covers it if truncation shows up there.Warn Diskonly sends a warning. The repair belongs in the Increase Disk Size job that follows it.Create Server Snapshotstops and starts the machine, but is left out of this PR.There is no scheduled sweep. The play runs only after the operations that cause the truncation.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XzhrGxqJ6nN5sVeK4swwt6