Backups: an archive is written under its final name, so one cut short looks like a backup and costs a good one at retention #1123
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1123
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
src/postulo/core/backup.py:501resolves the target to its final name (resolve_target, lines 450-457:postulo-backup-YYYYMMDD-HHMMSS.tar.gz) and line 548 opens it directly withtarfile.open(path, "w:gz"). Verification runs afterwards on the same path (line 571,verify_backup(path)). If anything raises in between, or the process is killed, or verification fails, the file stays under that name; nothing unlinks it (notry/exceptaround the write, andcore/backups.py:247-255take()only releases the lock).That file then matches
core/backups.py:50NAME, soarchives()(lines 160-173) lists it._describe(lines 129-157) reads only the manifest, which is the archive's first member (backup.py:549-552), so a truncated archive whose manifest was written is shown on the Backups page as an ordinary backup with its counts; it is marked only once somebody runs a check.apply_retention(lines 370-398) keeps the newestkeeparchives that are not uploads, so a broken archive counts as one of them and, on the next good run, one more good archive than intended is deleted. Withkeepat 1 or 2 that can leave the instance with a single good backup or none beside a broken one.manage.py backup <path>uses the samewrite_backupand has the same behaviour for a path the operator names.What would change
In
write_backup, write to a temporary name in the same directory that does not matchNAME(for exampletempfile.mkstemp(dir=path.parent, prefix=".postulo-backup-", suffix=".partial")), runverify_backupon it, and only thenos.replaceit to the final name; on any exception, unlink the temporary file and re-raise.os.replacewithin one directory is atomic, so a listed archive is always a complete, verified one. The same pattern is already used for the heartbeat (core/scheduler.py:60-66).Leftovers from a hard kill (SIGKILL, power loss) are dot-files that
NAMEignores; the scheduler's tidy can remove.postulo-backup-*.partialolder thanLOCK_SECONDS(6 hours,backups.py:54), since no backup runs that long.Tests: a
write_backupthat raises half way (patch_add_treeto raise) leaves no file matchingNAMEand no temporary file; a successful one leaves exactly the final file; retention with a stale.partialbeside it counts only real archives.What it saves or protects
medium: protects the kept backups: a run cut short (disk full, container killed, OOM) no longer leaves a file that the Backups page lists as normal and that retention counts as one of the newest N
Accessibility
none
Decisions
Found by the efficiency and reliability audit of 2026-10-30 (finding D3; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).