diff --git a/docs/misc/prune-example-interval.txt b/docs/misc/prune-example-interval.txt index f318e4f42..0106a69f2 100644 --- a/docs/misc/prune-example-interval.txt +++ b/docs/misc/prune-example-interval.txt @@ -7,9 +7,9 @@ to "thin out" over time while retaining most recent archives. Your backup script runs `borg create`, immediately followed by `borg prune`. Assume today is 2026-06-04 and you always start your backups at 16:00. You have -been creating backup archives starting at 16:00 on most days going back to late -2025. Today, `borg create` took a little longer than usual. It's 16:12 now and -you run `borg prune`. +been creating backup archives starting at 16:00, followed by pruning, on most +days going back to late 2025. Today, `borg create` took a little longer than +usual. It's 16:12 now and you run `borg prune`. You want Borg to keep one archive per day for one week, four weekly archives, one archive per month for five months, and two yearly backups. For that, you @@ -115,16 +115,22 @@ later reference time, e.g. `--since '2026-06-04 23:59:59'`. and `--keep-monthly`, too. The archive is effectively kept by `--keep-daily`, but how this affects other rules differs between *count*- and *interval*-based policies. For *interval*-based rules like `--keep-monthly 5m` it has no effect: -The rule simply keeps one archive fewer. +The rule simply keeps one archive fewer in that case. For *count*-based rules like `--keep-weekly 4` it has an effect: The policy -tells Borg to keep 4 weekly archives, and if 2026-05-31 is kept by another -rule already, Borg compensates by keeping an older archive instead. -Consequently, Borg will also keep the 2026-05-03 archive. +tells Borg to keep 4 weekly archives. Weekly slots are identified by ISO week +number; in this example the four kept archives cover weeks 18 through 21. If +2026-05-31 is kept by another rule already, Borg compensates by keeping an +older archive instead. Consequently, Borg will also keep the 2026-05-03 archive. -Since 2026-05-24 and 2026-03-31 were skipped, there are no perfect candidates -for that week and month. Borg chooses the next best candidate, so it keeps -2026-05-23 as the weekly and 2026-03-30 as the monthly candidate. +The week 22 slot is consumed by that same 2026-05-31 archive. Any other +archive from that week is not kept by weekly, even if it falls within the +daily window boundary. This is why 2026-05-27 is pruned: it has just fallen +out of the daily window, and no other rule covers it. + +Since 2026-05-24 and 2026-03-31 were skipped, Borg substitutes the next best +candidate for each: 2026-05-23 fills the week 21 weekly slot, and 2026-03-30 +fills the March monthly slot. The implementation of `--keep-monthly 5m` is somewhat special: Borg defines a month as a fixed 31-day period, independent of the actual calendar dates @@ -144,6 +150,6 @@ Intervals and counts can be mixed freely. Yearly retention in this example is done by retention count instead of intervals. A count rule paired with interval rules behaves just as it would if all preceding rules were also counts: Archives already kept by earlier rules are excluded from consideration. In this -example there is only one "true" yearly candidate, so the oldest archive at -2025-11-15 is kept. This oldest archive will be kept until the rolling backup -scheme reaches "steady state" (when all retention rules are fully satisfied). +example, every yearly slot is consumed by earlier rules; no true yearly +candidate remains, so the oldest archive at 2025-11-15 is kept to preserve +rolling backup semantics. diff --git a/src/borg/testsuite/archiver/prune_cmd_test.py b/src/borg/testsuite/archiver/prune_cmd_test.py index 23c12029d..6b1617f73 100644 --- a/src/borg/testsuite/archiver/prune_cmd_test.py +++ b/src/borg/testsuite/archiver/prune_cmd_test.py @@ -123,9 +123,7 @@ def test_prune_repository_example_interval(archivers, request, backup_files): # All timestamps are at exactly 16:00 UTC (matching the example). # Backups on most days from 2025-11-15 to 2026-06-04, with skips on - # 2026-03-31 and 2026-06-03. At the inclusive interval boundaries, - # 2026-05-28 (1w before --since) and 2025-12-31 (155d before --since) - # are kept. + # 2026-03-31, 2026-05-24, and 2026-06-03. archive_dates = [ (2025, 11, 15), (2025, 12, 31), @@ -133,6 +131,11 @@ def test_prune_repository_example_interval(archivers, request, backup_files): (2026, 2, 28), (2026, 3, 30), (2026, 4, 30), + (2026, 5, 3), + (2026, 5, 10), + (2026, 5, 17), + (2026, 5, 23), + (2026, 5, 27), (2026, 5, 28), (2026, 5, 29), (2026, 5, 30), @@ -153,6 +156,7 @@ def test_prune_repository_example_interval(archivers, request, backup_files): "--dry-run", "--since=2026-06-04T16:00:00+00:00", "--keep-daily=1w", + "--keep-weekly=4", "--keep-monthly=5m", "--keep-yearly=2", ) @@ -166,18 +170,27 @@ def test_prune_repository_example_interval(archivers, request, backup_files): "backup_2026-05-31", "backup_2026-05-30", "backup_2026-05-29", - # 2026-05-28 is at the inclusive boundary (exactly 1w before --since). "backup_2026-05-28", ] for i, name in enumerate(daily_kept, 1): assert re.search(rf"Keeping archive \(rule: daily #{i}\):\s+{name}", output) + # Weekly W22 slot is consumed by 05-31 (already kept by daily), + # so weekly reaches back to W18 to fill all 4 slots. + weekly_kept = [ + "backup_2026-05-23", # W21 — no Sunday candidate (05-24 skipped) + "backup_2026-05-17", # W20 + "backup_2026-05-10", # W19 + "backup_2026-05-03", # W18 + ] + for i, name in enumerate(weekly_kept, 1): + assert re.search(rf"Keeping archive \(rule: weekly #{i}\):\s+{name}", output) + monthly_kept = [ "backup_2026-04-30", "backup_2026-03-30", "backup_2026-02-28", "backup_2026-01-31", - # 2025-12-31 is at the inclusive boundary (exactly 155d before --since). "backup_2025-12-31", ] for i, name in enumerate(monthly_kept, 1): @@ -186,6 +199,11 @@ def test_prune_repository_example_interval(archivers, request, backup_files): # No true yearly candidates remain; only the oldest archive fills the slot. assert re.search(r"Keeping archive \(rule: yearly\[oldest\] #1\):\s+backup_2025-11-15", output) + # 05-27 was kept by daily yesterday (window started 05-26) but falls out + # today (window shifted to 05-28). W22 slot is consumed by daily-held 05-31, + # so weekly doesn't save it either. + assert re.search(r"Would prune:\s+backup_2026-05-27", output) + def test_prune_quarterly(archivers, request, backup_files): # Example worked through by hand when developing the quarterly