Skip to content

fix(backup): a failed full backup says why - #1670

Merged
frahlg merged 1 commit into
masterfrom
fix/backup-failure-reason
Oct 21, 2026
Merged

frahlg merged 1 commit into
masterfrom
fix/backup-failure-reason

Conversation

@frahlg

@frahlg frahlg commented Oct 16, 2026

Copy link
Copy Markdown
Member

Problem

On 16 October 2026 a full backup on the home box (homelab-rpi, Raspberry Pi 4, Core v0.144.1-beta.1) ran from 12:05 until about 14:05 and then failed. The journal has no line about it, and the CLI had already been stopped. Core kept the reason only in memory, and the update to v0.145.0-beta.1 at 15:37 wiped it.

What happened

Read-only evidence from the box (journal, file times, df):

  1. A backup on 13 October left 1.37 GB in /dev/shm. systemctl stopped Core at 10:30:29 while a backup was copying. The process exited at 10:30:34, so the deferred cleanup never ran. /dev/shm/ftw-backup-4218622654.raw (1,372,872,704 bytes) and its -journal still have that mtime. Nothing removes them before a reboot.
  2. So the 16 October copy went to the SD card. backupScratchFile needs the source size plus 64 MiB in /dev/shm. With today's files (state.db 283 MB, history.db 1.30 GB, WAL) that is about 1.67 GB. /dev/shm holds 1.99 GB, but with the leftover file only 616 MB is free, so the raw copy fell back to <backups>/.ftw-backup-stage-*/database.gz.raw.tmp and logged nothing. On the SD card, every 256 KiB commit fsyncs the same card that holds history.db. That explains the 1,600–2,400 rows/s. It also explains why the history writer (write_bucket 6.8–11.7 s, read_bucket 5–6 s) and forecast calibration (3 s budget) lost their deadlines from 12:07 on. The comment above backupScratchFile already describes this effect on the same box.
  3. The live copy then hit its two-hour limit. liveBackupTimeout is 2 h for the state, history and gzip steps. The backup directory's mtime is 14:05:44.205, when Create removed its stage directory, two hours after the 12:05 start. The writer-lock timeout at 14:05:49 most likely comes from history maintenance resuming and the stage file being removed from the card. The error was backup history: copy ts_buckets: context deadline exceeded. The handler put it in progress.error and logged nothing.

The 10 October backups (2 h 40 min in all) predate the leftover file, so their raw copy most likely fit in /dev/shm.

TestLiveBackupPastItsLimitNamesTheLimitAndTable reproduces step 3 with a one-second limit and slow batches. Without this change its reason reads backup history: copy history_hot: context deadline exceeded, the same bare form as on the box.

Change

  • Core logs full backup started, full backup created (id, size, elapsed) and full backup failed (err, phase, table, rows_done, elapsed).
  • A live backup that runs out of time says live backup reached its time limit of 2h0m0s: …. A caller that leaves still gets plain context canceled.
  • GET /api/backups keeps the failed run's table and row count beside the reason. A new run clears the old record first, so an early failure does not inherit the last run's table.
  • ftw backup prints Last try: failed: <reason> before it starts when Core still holds a failure.
  • When the raw copy does not fit in /dev/shm, Core warns that it is copying on the data disk, with need_bytes, free_bytes and backup_scratch_in_shm_bytes. On 16 October that line would have pointed straight at the leftover file.

Why the speed stays as it is

The hours on 16 October came from the SD-card fallback, and the leftover file caused that. Removing the file and preventing new ones restores the path that worked on 10 October. That cleanup is its own change.

I found no speed-up I could show to be safe. Larger batches make each SD-card fsync bigger, and the comment above backupScratchFile records a goal save over 2 s even at 16 KiB batches. A page copy (SQLite's online backup API) changes how much IO hits the card per second. Whether history writes and forecast reads keep their deadlines meanwhile only shows on a Pi with an SD card, and the home box is read-only for this work. A laptop SSD number would not answer that question.

One risk remains: the /dev/shm path needs about 1.67 GB today and has 1.99 GB, and history.db grows. Once it no longer fits, every live backup on this box takes the SD-card path and will likely hit the limit again.

Overlap

#1615 also edits api_backups.go. The hunks differ, but both add an import after "fmt", so whichever lands second needs a one-line merge.

Tests

  • go test ./internal/state/ -run 'TestLiveBackupPastItsLimit|TestCanceledBackupIsNotBlamed|TestBackupScratchOnDisk'
  • go test ./internal/api/ -run TestFailedBackupLogsAndReportsItsReason. Without the reset, its early-failure check fails with Table:ts_buckets RowsDone:4700000.
  • go test ./internal/ftwcli/ -run TestBackupNamesTheLastFailureAndItsOwn
  • make verify: vet, test and build clean on macOS arm64. The /dev/shm warning test points backupShmDir at a temporary directory, so it runs without a tmpfs.

No backup, update or write ran on the home box.

🤖 Generated with Claude Code

The home box's backup on 16 October 2026 stopped at the two-hour limit
for a live copy and logged nothing. Its raw copy had fallen back to the
SD card because a backup cut short on 13 October left 1.37 GB in
/dev/shm.

Core now logs when a backup starts, ends and fails, with the phase,
table and rows it reached. A backup stopped by the limit names it.
GET /api/backups keeps the failed run's table and rows, and a new run
clears the old record. ftw backup prints the last failure before it
starts. A raw copy that does not fit in /dev/shm logs a warning with the
space earlier backups' scratch files still take there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 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 ✅ Completed 2026-10-16T16:58:57.129610Z 6e2546b PR opened
ℹ️ 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: 6e2546bccd

ℹ️ 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".

s.deps.State.ReportBackupProgress(state.BackupProgress{Phase: "failed", Error: err.Error()})
last := s.deps.State.BackupProgress()
slog.Error("full backup failed", "err", err, "phase", last.Phase, "table", last.Table, "rows_done", last.RowsDone, "elapsed", elapsed)
s.deps.State.ReportBackupProgress(state.BackupProgress{Phase: "failed", Table: last.Table, RowsDone: last.RowsDone, Error: err.Error()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist the failed-backup status across restarts

When Core restarts or updates after a backup failure but before the next backup begins, this call stores the failure only in Store.backupStatus, an in-memory field that is empty in the new process. Consequently, GET /api/backups loses the reason, table, and row count, and the new ftw backup message cannot show the last failure—the exact update-after-failure scenario this change is intended to diagnose. Persist and reload the last outcome, clearing it when the next backup starts.

AGENTS.md reference: AGENTS.md:L22-L25

Useful? React with 👍 / 👎.

frahlg commented Oct 18, 2026 •

Copy link
Copy Markdown
Member Author

The outstanding restart finding still applies to current head 6e2546bc: failed-backup details flow through Store.backupStatus, which is an in-memory BackupProgress guarded by backupStatusMu. BackupProgress returns that field; a new Store starts with no retained outcome.

Keep this PR open until the last failure is persisted and reloaded. Add a close/reopen regression that checks the error/table/row count in the API and CLI before another backup starts, then verifies that beginning a new attempt clears the old failure. The existing same-process tests do not establish restart persistence.

@frahlg
frahlg merged commit df21447 into master Oct 21, 2026
17 checks passed
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.

1 participant