8000 feat: add clean_broken_retention CLI command by Slach · Pull Request #1371 · Altinity/clickhouse-backup · GitHub
[go: up one dir, main page]

Skip to content

feat: add clean_broken_retention CLI command - #1371

Merged
Slach merged 20 commits into
masterfrom
clean_broken_retention
May 20, 2026
Merged

Slach merged 20 commits into
masterfrom
clean_broken_retention

Conversation

@Slach
@Slach Slach commented May 13, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

New CLI command clean_broken_retention that walks top-level of remote path and object_disks_path, batch-deletes (with retry) every entry that is not a live backup and is not excluded by --exclude globs and is matched by --include globs (if provided). Object disk orphans are deleted in parallel with progress tracking. Pass --commit to actually delete; without it the command only logs what would be deleted.

  • Whitelist = live remote backups ∪ --exclude globs (path.Match syntax, repeatable); --include globs scope cleanup to matching names only
  • Dry-run by default; --commit performs the actual deletes
  • Reuses BackupDestination.RemoveBackupRemote (for path) and cleanBackupObjectDisks (for object_disks_path) — both already use batched delete with retry/exponential-backoff
  • Motivation: clean up orphans left behind by failed retention runs (e.g. the GCS 503 scenario from batching deletion failures need retry #1356) — the retry fix prevents new orphans, this command sweeps up existing ones

Usage

# preview what would be deleted
clickhouse-backup clean_broken_retention

# scope cleanup to specific name patterns
clickhouse-backup clean_broken_retention --include='stale-*'

# preserve backups via exclude globs, then actually delete
clickhouse-backup clean_broken_retention --exclude='prod-*' --exclude='snapshot-2026-??-*' --commit

Test plan

  • CI green (unit + integration)
  • Manual dry-run on a bucket containing a known orphan — verify it is listed without being deleted
  • Manual --commit run — verify the orphan is removed from both path and object_disks_path, batched, with retry on transient 5xx

Slach added 2 commits May 13, 2026 23:24
Walks top-level of remote `path` and `object_disks_path` and batch-deletes
(with retry, via the existing BatchDeleter pipeline) every entry that is
not present in the live BackupList and not matched by any --keep=<glob>.

Dry-run by default; --commit performs the deletes. Useful for cleaning
up orphans left behind by failed retention runs (e.g. the GCS 503
scenario from #1356).
Plant orphans directly under /minio/data/clickhouse for both `path`
and `object_disks_path`, then verify:
  - dry-run lists them without deleting,
  - --commit removes them from both locations,
  - --keep=<glob> preserves matching entries,
  - the live backup is never touched.
@Slach Slach modified the milestone: 2.7.0 May 13, 2026
Slach added 17 commits May 13, 2026 23:51
…ckends

Table-driven, with one sub-test per backend:
  - S3 (minio), SFTP (sshd), FTP (proftpd), GCS_EMULATOR (fake-gcs-server)
    use direct container-FS plant/assert,
  - AZBLOB plants via az-cli docker run against azurite,
  - real GCS and COS skip themselves unless GCS_TESTS / QA_TENCENT_SECRET_KEY
    is set (their plant helpers fail loudly if reached without infra).

For each backend: create a real backup that must survive, plant 3 orphans
in path/object_disks_path, verify dry-run lists them without deleting,
verify --commit + --keep glob deletes only unmatched orphans, then a
second --commit clears the rest.
…roken_retention

GCS: spins up google/cloud-sdk:slim with --volumes-from clickhouse-backup
to access /etc/clickhouse-backup/credentials.json, authenticates the
service account, and uses gsutil cp/ls.

COS: spins up amazon/aws-cli:latest with --endpoint-url pointing at the
regional COS endpoint and QA_TENCENT_SECRET_ID/KEY as AWS credentials,
since COS supports the S3 API. Setup hook renders config.yml from the
copied template via envsubst.

Both run only when their credential env var is set.
- Collapse the three container-FS factories (S3/SFTP/FTP/GCS_EMULATOR)
  into a single containerFSCase helper.
- Extract dockerRunSh and an azList closure to remove repetition in the
  AZBLOB case; reuse a single gsutil/awsRun wrapper for GCS and COS.
- Drop the redundant storageType field; derive the table name from name.
- Loop the plant+assertExists step over a small table instead of
  open-coding four calls.
- Pull repeated keep-glob string into a const, drop fmt.Sprintf with no
  format args.
Signed-off-by: slach <bloodjazman@gmail.com>
…ash in Walk names

The command previously left orphans untouched because:
1. `BackupList(ctx, false, ...)` uses the on-disk metadata cache; on the
   second invocation (e.g. dry-run → --commit) the cache returns orphan
   directories with Broken="", so my Broken!="" filter let them into the
   keep-set and zero orphans were detected. Now passes parseMetadata=true
   so every top-level entry is stat'd for metadata.json on each call.
2. `bd.Walk("/", false, …)` emits names with a leading slash on S3 (from
   TrimPrefix mismatch); the previous `strings.Contains(name, "/")` filter
   rejected them as nested. Switched to strings.Trim("/") so leading and
   trailing slashes are both stripped before the top-level check.

test/integration:
- Split TestCleanBrokenRetention into one top-level test per backend so
  each can be run independently via RUN_TESTS=TestCleanBrokenRetention<Backend>.
- S3 case now plants orphans via 'mc cp' instead of raw file writes —
  MinIO only sees objects placed through its S3 API; direct files on disk
  are invisible to ListObjectsV2 and the cleanup batch finds nothing.
Signed-off-by: slach <bloodjazman@gmail.com>
…lean_broken_retention

# Conflicts:
#	test/testflows/clickhouse_backup/tests/snapshots/cli.py.cli.snapshot
Signed-off-by: slach <bloodjazman@gmail.com>
…ntics

BackupList discovers all top-level entries under backup path and marks
directories without metadata.json as "broken" backups. Since broken
backups must be kept (they may be in-progress uploads), entries under
the backup path can never be true orphans.

Genuine orphans only exist under object_disks_path, which BackupList
does not scan. The tests now correctly:

- Plant a broken entry under pathRoot, assert it is preserved throughout
- Plant orphans only under objRoot, assert they are cleaned
- Use clean_remote_broken to clean up the broken entry at the end
Add NewErrNotFound(key) helper that wraps ErrNotFound with the actual
key path, so 'key not found: backup_name/shadow/db/table/part.tar'
replaces the uninformative 'key not found' in download/upload errors.
…place --keep with --exclude/--include

- findOrphanTopLevelNames now scans getBackupPath() instead of "/"
  (avoids walking the entire bucket root on shared storage)
- Replace --keep flag with --exclude (preserve matched orphans) and
  --include (scope cleanup to matching names only)
- Test isolation: all TestCleanBrokenRetention* use --include=cbr_*
  to avoid interfering with parallel tests in the same backend bucket
…s-test deletion

CleanRemoteBroken previously deleted all broken remote backups without
any name filtering. When multiple CI matrix jobs share the same GCS
bucket, a concurrent TestCleanBrokenRetentionGCS could delete mid-upload
backups from other tests (e.g. TestGCS) that temporarily lack
metadata.json.

Add --include flag (same path.Match syntax as clean_broken_retention) so
callers can scope deletion to a name pattern. API/server callers pass
nil for backward compatibility.

Update the integration test to use --include=cbr_* so clean_remote_broken
only touches backups owned by the retention test.
- Include CLICKHOUSE_VERSION in cbr_* glob patterns and backup names
  so parallel CI jobs with different CH versions don't interfere
  with each other in shared GCS/Azure buckets
- Move DROP TABLE to t.Cleanup via docker exec instead of Go client,
  avoids 'connection is closed' when defer runs after env.Cleanup
- Add shell command to gsutil error message for easier debugging
@coveralls
coveralls commented May 19, 2026 •
Copy link
Copy Markdown

Coverage Report for CI Build 26110299144

Warning

No base build found for commit 1a5b06b on master.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 63.609%

Details

  • Patch coverage: 86 uncovered changes across 4 files (227 of 313 lines covered, 72.52%).

Uncovered Changes

File Changed Covered %
pkg/backup/delete.go 257 188 73.15%
pkg/storage/cos.go 11 0 0.0%
pkg/backup/backuper.go 15 12 80.0%
pkg/storage/ftp.go 16 13 81.25%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 19914
Covered Lines: 12667
Line Coverage: 63.61%
Coverage Strength: 35319.54 hits per line

💛 - Coveralls

@Slach
Slach merged commit 0fba0d5 into master May 20, 2026
53 of 54 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.

2 participants

0