Repository navigation
fix(backup): retry the scheduled base backup before failing the night - #218
Merged
Merged
Conversation
A base backup streams gigabytes through many multipart uploads, and wal-g aborts the whole push on the first part the destination refuses. Its S3 client (aws-sdk-go v1 in wal-g 3.0.8) retries only what the SDK classes as transient — 5xx, 429, throttling codes, connection resets — so a bare HTTP 400 from an S3-compatible destination fails the push outright. On a production host using Hetzner Object Storage, three of six nightly pushes were lost that way, each to a single part, while the same configuration succeeded on the other nights and on demand. The unit had one ExecStart and nothing to try again with. The backup unit now runs a host-side script, installed beside the verify script, that attempts the push up to three times, a minute and then five minutes apart, and exits with the last attempt's status. The loop lives in POSIX sh rather than in Restart= because systemd re-runs every ExecStart on a restart — retention included — and its start rate limiter bounds restarts by time rather than count, so a push slower than the limiter's window would retry forever. Retention stays a separate ExecStart, so it never runs after a push that failed every attempt. The flock still wraps the whole script, so an interactive `ob backup` waits rather than interleaving. Claude-Session: https://claude.ai/code/session_01VubhEStkwQngp9TwWiH9T7
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No unresolved review comments remain.
Review effort: Lite
Findings: None
What changed in this PR
Improves scheduled WAL-G base backups by retrying transient failures before the unit fails.
Changes:
- Adds a host-side retry script with staged delays.
- Runs retention only after a successful backup.
- Adds tests and documentation.
| File | Description |
|---|---|
site/src/content/docs/guides/back-up-a-database.mdx |
Documents scheduled backup retries. |
internal/engine/schedule_test.go |
Tests retry behavior and unit ordering. |
internal/engine/backup_schedule.go |
Generates retry scripts and updated units. |
internal/app/names.go |
Adds the retry script path helper. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
What this changes
The scheduled base backup unit (
onebox-backup-<service>-backup.service) ranwal-g backup-pushas a singleExecStartunder flock. One refused multipart part aborts a whole push, and nothing tried again, so a transient fault at the destination lost the night.Observed on a production host using Hetzner Object Storage (wal-g 3.0.8 from the pinned PostgreSQL image, default upload settings): on three of six consecutive nights a ~14.6 GB push failed 4–9 minutes in with
(parts 4, 6 and 10 on different nights) while the same configuration and credentials succeeded on the other nights and on demand. WAL archiving was unaffected throughout.
Why nothing retried, from the sources:
internal/storage_tar_ball.go,Fatalf("Unable to continue the backup process because of the loss of a part %d")).BadRequestis not retryable (aws/request/retryer.go). wal-g's own retryer (pkg/storages/s3/retryer.go) only adds "connection reset by peer" / "connection refused".WALG_S3_MAX_RETRIES(default 15) changes how many times a retryable error is retried, not which errors are.BadRequest, so it would not obviously cover it either.So there is no documented wal-g or SDK knob in a released version that makes this part upload retry. This PR adds the layer Onebox owns: a bounded retry of the whole push.
The backup unit now runs a host-side script, installed beside the existing verify script (
backup-<service>.sh), that attemptsbackup-pushup to three times, a minute and then five minutes apart, and exits with the last attempt's status. Retention stays a separateExecStart, so it never runs after a push that failed every attempt. The flock still wraps the whole script, retries included, so an interactiveob backupwaits rather than interleaving.It is a script rather than
Restart=because systemd re-runs everyExecStarton a restart (retention included), and its start rate limiter bounds restarts by time rather than count, so a push slower than the limiter's window would restart forever.No issue was filed first: this is a bug fix diagnosed from the host's journal.
Closes #
Why this is correct
TestTheScheduledBaseBackupRetriesBeforeFailingTheUnitexecutes the rendered script undershwith a stand-in push that fails a chosen number of times: one run when the first attempt succeeds, two when the first fails, three runs and the push's own exit status (7) when every attempt fails, with each attempt's reason on stderr. It also checks the installed script runs the realdocker exec … backup-push, carries the real waits (set -- 60 300,attempts=3), has noset -e(which would leave the loop on the first failure), and passessh -n. It fails against the previous unit, which had no script.TestTheScheduledBackupUnitRetriesThePushAndNotTheRetentionrunsSyncBackupSchedulesagainst a protected PostgreSQL fixture and asserts the unit's firstExecStartis the script under the same flock, thatdelete retainfollows as its ownExecStart, that the unit itself contains nobackup-pushand noRestart=, and that the installed script pushes this service's container.just check(fmt, vet, fullgo test ./..., generated docs, site build). Not run: the Docker e2e suite; no change touches a path it covers differently.Effect on the safety envelope
A scheduled push may now run up to three times per firing, and the backup flock can be held about six minutes longer between attempts, so an interactive
ob backupstarted in that window waits longer. Nothing new is done to a running system; retention, the verify unit and the interactive commands are unchanged. Existing hosts get the new unit and script on their nextob service applyorob backup enable; until then they keep the old unit. No/status/capabilitieschange.Checklist
just checkpasses locally.just checkverifies this).https://claude.ai/code/session_01VubhEStkwQngp9TwWiH9T7