From 89c6a2ccccacfe98ae61d3f5afdf09783b2c453b Mon Sep 17 00:00:00 2001 From: Hugo Wallenburg Date: Sat, 20 Jun 2026 18:03:55 +0200 Subject: [PATCH] Improves interval example Makes specific notes on week numbers, clears some ambiguity in the yearly candidate wording, and more. The matching test was previously missing assertions on the weekly archives, now fixed along with better explanations. --- docs/misc/prune-example-interval.txt | 32 +++++++++++-------- src/borg/testsuite/archiver/prune_cmd_test.py | 28 +++++++++++++--- 2 files changed, 42 insertions(+), 18 deletions(-) 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