Skip to content

watch: keep backup chain when only post-upload steps fail - #1622

Open
Slach wants to merge 8 commits into
masterfrom
issue-1535-watch-retention-error
Open

Slach wants to merge 8 commits into
masterfrom
issue-1535-watch-retention-error

Conversation

@Slach

@Slach Slach commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1535

Problem

When a backup is fully uploaded but a step after the upload fails (remote retention RemoveOldBackupsRemote, local retention, or --delete-source removal), create_remote returned an error and watch treated it like a failed upload: prevBackupName was not advanced (next increment diffed from a stale base), createRemoteErrCount grew, and watch eventually aborted with "too many errors".

Change

  • Post-upload failures are wrapped in an unexported postUploadError; watchBackupUploaded(err) classifies them.
  • uploadEpilogue now runs all three post-upload steps even if an earlier one fails and joins their errors.
  • watch (interval mode and --schedule): on a post-upload error the backup counts as uploaded — chain advances, error doesn't count towards aborts, still logged at error level and reported to /backup/status, system.backup_actions and the callback.
  • --schedule: if full_type=rebase fails after a post-upload error, both errors are reported and the backup is treated as not uploaded (no delete_previous_cycle).
  • Embedded ON CLUSTER: the initiator still waits for workers before returning a post-upload error; a worker failure is returned as a real failure.
  • create_remote --streaming: a post-upload failure no longer reports "partial backup ... delete it from both".
  • One-shot create_remote / upload keep the non-zero exit code.

Known limitation

Post-upload failures on embedded ON CLUSTER worker nodes still count as failed uploads (the error type is lost through system.backup_actions); this errs on the safe side. Metrics: failed_create_remotes keeps growing while backups are uploaded (documented in ChangeLog).

Tests

  • Unit: TestWatchBackupUploaded, TestNewPostUploadError, TestRebaseFailedError; go vet, go test ./pkg/backup/... ./pkg/server/... pass.
  • TestFlows: new scenario /clickhouse backup/watch/retention failure keeps chain (forces remote retention failure via stale embedded backups with a missing embedded_backup_disk) — passes locally (71/71 steps); fails on pre-fix code because watch aborts with "too many errors" before any increment exists.
  • Reviewed with a multi-model review (rejudge); findings applied.

🤖 Generated with Claude Code

Slach and others added 8 commits October 18, 2026 22:11
RemoveOldBackupsRemote runs after the new backup is completely uploaded.
Its failure made CreateToRemote return an error which watch treated as a
failed upload: prevBackupName was not advanced, so the next increment was
diffed from a stale base although the fresh backup exists on remote, and
createRemoteErrCount grew until watch aborted with "too many errors".

The retention error is now wrapped into remoteRetentionError, and watch
(interval and --schedule modes) uses watchBackupUploaded() to advance the
chain and not count the iteration towards abort, while still logging the
error and reporting it via status and callback. With --schedule the
full_type=rebase and delete_previous_cycle steps proceed as well. One-shot
create_remote/upload keep returning the error (non-zero exit code).

Fixes #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local retention (backups_to_keep_local) and --delete-source also run
after the new backup is completely on remote storage, so their failure
must not make watch rebuild the chain from a stale base or abort with
"too many errors" either. remoteRetentionError becomes postUploadError
and wraps these errors too, including the local retention at the end of
create_remote --streaming. One-shot create_remote/upload still fail.

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two old remote backups tagged `embedded` are put to FTP and watch runs
with an embedded_backup_disk that doesn't exist, so every
backups_to_keep_remote retention fails after the new backup is uploaded
(RemoveBackupRemote and object disk cleanup errors are only warnings,
this is a deterministic failing path). watch must keep running until
killed by `timeout`, log the retention error and upload the increment
with required_backup = the full backup of the first iteration.

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
watch reloads the config file on every iteration (watch.go LoadConfig)
without re-applying --env overrides, so REMOTE_STORAGE / retention /
embedded_backup_disk passed with --env were lost after the first reload:
the upload failed with remote_storage "none" and backups_to_keep_remote=0
from the file made watch abort after the first error. Use
config_modifier like other scenarios and restore the config in cleanup.

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`delete remote` of the planted embedded-tagged backups fails for the
same reason as the retention (embedded_backup_disk doesn't exist), so
cleanup left them on remote storage. Delete them with FTP DELE/RMD the
same way they were planted, and restore the original config even when
the final assertion fails.

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- embedded ON CLUSTER initiator: on a post-upload error still wait for
  the worker nodes, a worker failure is returned unwrapped, so watch
  never advances the chain (or deletes the previous schedule cycle) on a
  backup which is incomplete cluster-wide
- uploadEpilogue: remote retention, local retention and --delete-source
  all run even when a previous step failed, errors are joined into one
  postUploadError, so a permanent remote retention failure doesn't leak
  a local backup per watch iteration
- create_remote --streaming: a post-upload failure is no longer reported
  as a partial backup to resume or delete, shadow cleanup and local
  retention still run
- --schedule full_type=rebase: a rebase failure keeps the post-upload
  error message but stays "not uploaded"
- ChangeLog: never-abort and metrics behavior, worker-side limitation

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- run watch in the background with PID / exit code files and poll
  `list remote` until an increment with uploaded metadata.json appears
  (bounded deadline) instead of a fixed 50s `timeout`, so a slow
  increment upload isn't killed midway; the check is that watch was
  still running, the exit code after our SIGTERM depends on its handler
- parse `list --format=json remote` instead of text columns
- cleanup stops watch by its PID file and always restores the config
- CREATE TABLE IF NOT EXISTS for reruns after a failed cleanup

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The background job of the interactive testflows bash printed
`[1]+ Done ...` into the output of a later command, which broke
parsing in cleanup. Start it from a child bash, the same way
BackupNode.start_server does.

Refs #1535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Slach Slach added this to the 2.8.2 milestone Oct 19, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 39210929648

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage at 70.287% (no base build to compare)

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 10 uncovered changes across 3 files (39 of 49 lines covered, 79.59%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
pkg/backup/watch_schedule.go 19 12 63.16%
pkg/backup/upload.go 15 13 86.67%
pkg/backup/create_remote.go 9 8 88.89%
Total (4 files) 49 39 79.59%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 29378
Covered Lines: 20649
Line Coverage: 70.29%
Coverage Strength: 34366.16 hits per line

💛 - Coveralls

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.

watch: distinguish post-upload retention failure from upload failure

2 participants