Skip to content

Commit 9dc9628

Browse files
mattgwagnerclaude
andcommitted
fix(backup): prune by timestamp so weekly fulls survive retention
Retention sorted backups lexically by name. Because "logs-full-" sorts before "logs-incr-" ('f' < 'i') regardless of date, every freshly created weekly full was treated as the oldest backup and pruned in the same run that created it. The original base full stayed pinned (protected as the base of every incremental), so the chain never re-based and incrementals accumulated as ever-growing deltas — one prod volume reached 420GB / 2.6x the live data. Sort local and R2 backup lists by the trailing YYYY-MM-DD-HHMMSS timestamp instead, so the newest full is correctly the newest entry and ages out only once its incrementals do. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c2f37fa commit 9dc9628

1 file changed

Lines changed: 11 additions & 2 deletions

File tree

backup/backup.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,13 @@ collect_protected_fulls() {
136136

137137
# Prune local backups (chain-aware)
138138
log "Checking local retention (keeping $BACKUP_RETAIN_LOCAL)..."
139-
LOCAL_BACKUPS=$(find "$BACKUP_DIR" -maxdepth 1 -type d \( -name "logs-full-*" -o -name "logs-incr-*" -o -name "logs-[0-9]*" \) 2>/dev/null | sort)
139+
# Sort chronologically by the trailing YYYY-MM-DD-HHMMSS timestamp, NOT lexically.
140+
# A plain `sort` orders "logs-full-*" before "logs-incr-*" ('f' < 'i') regardless of
141+
# date, so a freshly created full looks like the oldest backup and gets pruned the
142+
# moment it's made — leaving the original base full pinned forever and the chain
143+
# unable to re-base. Keying on the last 17 chars (the timestamp) fixes the ordering.
144+
LOCAL_BACKUPS=$(find "$BACKUP_DIR" -maxdepth 1 -type d \( -name "logs-full-*" -o -name "logs-incr-*" -o -name "logs-[0-9]*" \) 2>/dev/null \
145+
| awk -F/ '{n=$NF; print substr(n, length(n)-16) "\t" $0}' | sort | cut -f2-)
140146
LOCAL_COUNT=$(echo "$LOCAL_BACKUPS" | grep -c "." || true)
141147

142148
if [ "$LOCAL_COUNT" -gt "$BACKUP_RETAIN_LOCAL" ]; then
@@ -158,7 +164,10 @@ fi
158164

159165
# Prune R2 backups (chain-aware)
160166
if has_r2; then
161-
R2_DIRS=$(rclone lsd "r2:${R2_BUCKET}/" --config "$RCLONE_CONF" 2>/dev/null | awk '{print $NF}' | grep "^logs-" | sort)
167+
# Same timestamp-keyed ordering as local pruning (see note above) so newly
168+
# created fulls aren't mistaken for the oldest backup and purged from R2.
169+
R2_DIRS=$(rclone lsd "r2:${R2_BUCKET}/" --config "$RCLONE_CONF" 2>/dev/null | awk '{print $NF}' | grep "^logs-" \
170+
| awk '{print substr($0, length($0)-16) "\t" $0}' | sort | cut -f2-)
162171
R2_COUNT=$(echo "$R2_DIRS" | grep -c "^logs-" || true)
163172

164173
if [ "$R2_COUNT" -gt "$BACKUP_RETAIN_OFFSITE" ]; then

0 commit comments

Comments
 (0)