Commit graph

10378 commits

Author SHA1 Message Date
Thomas Waldmann
5de314ba2f
cache: never build a partially merged chunk index, retry on vanished fragment
A concurrent repack_chunkindex (another client closing its cache, under a
shared lock) stores a merged replacement fragment and then deletes the small
source fragments. A reader that listed index/* before the replacement existed
and loaded the fragments after the deletion would silently skip the vanished
fragment and return a partially merged chunk index: chunks that exist in the
repo become invisible (spurious ObjectNotFound on read, lost deduplication,
duplicate chunk copies written into new packs).

Make the fragment merge all-or-nothing: if any listed fragment fails to load,
discard the partial merge, re-list and retry - the fresh listing contains the
replacement fragment. If no complete, consistent fragment set can be read
within CHUNKINDEX_MERGE_ATTEMPTS, fall back to the slow (but correct) index
rebuild from the pack headers instead of returning an incomplete index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 23:37:29 +02:00
Thomas Waldmann
e34722a933
cache: make full index write explicit in build_chunkindex_from_repo 2026-07-06 22:33:54 +02:00
Thomas Waldmann
83d80d3621
files cache: drop entries referencing missing chunks
The files cache stores each file's chunk list "compressed" to chunk-index
hash-table positions, valid only while the index is unmutated. When a backup
runs out of space and aborts, pending chunks are rolled back out of the
index; a files-cache entry memorized earlier still points at a now-stale
position, so decompress_entry's self.chunks[id] raises KeyError. This crashed
close() while it was cleaning up the already-failed backup, masking the real
ENOSPC error with a "Key not found" traceback.

Guard the two call sites that resolve cached entries against the index, both
mirroring the existing compress_entry KeyError handling in _read_files_cache:
- _write_files_cache drops the unresolvable entry (the file is re-chunked next
  backup) instead of raising.
- file_known_and_unchanged treats the file as unknown so it gets re-chunked.

Reproduced on a space-limited ramdisk (borg create -> ENOSPC): the crash is
gone and the genuine "No space left on device" error surfaces instead; the
repo remains consistent (check ok) and recoverable (compact + backup ok).

Add a regression test that memorizes a file, deletes its chunk from the index,
and asserts neither a cache lookup nor the files-cache save raises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:33:52 +02:00
Thomas Waldmann
fdd4c17189
Make repack idempotent: gate index deletion on presence, not upload
write_chunkindex_to_repo gated deletion of superseded index fragments on
whether it actually uploaded the replacement (stored_anything). Combined
with repack's force_write=True, every repack re-uploaded byte-identical
merged fragments; and had repack simply dropped force_write, a crash
between store and delete would leave the sources undeleted forever (the
re-derived fragments dedupe-skip, stored_anything stays False, deletion
never runs) -- small fragments piling up and being re-merged on every run.

Gate deletion on new_hashes (the fragment set making up the index just
written) instead. A dedupe-skipped fragment means its content is verifiably
already in the repo, so deleting the superseded fragments is safe; an empty
write (new_hashes empty) still skips deletion, so we never leave the repo
without an index. repack now passes force_write=False, gaining idempotence
with no redundant uploads.

Add a regression test covering the crash/concurrent-repack recovery case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:33:51 +02:00
Thomas Waldmann
fa3263d77b
Repack chunks index into entry-count-bounded fragments
Replace the unconditional "collapse all index/* fragments into one
all-in-one index" behavior with an entry-count-based repacking policy that
keeps each fragment within [CHUNKINDEX_FRAGMENT_ENTRIES_MIN,
CHUNKINDEX_FRAGMENT_ENTRIES_MAX] entries where possible.

- write_chunkindex_to_repo now always splits its output into fragments of at
  most MAX entries (streamed, so memory stays bounded), collecting all new
  hashes so delete_other/delete_these never delete a fragment just written.
- repack_chunkindex() merges small (< MIN) fragments, deferring until they can
  seal a full fragment (sum >= MIN) or too many piled up (> SMALL_FRAGMENT_CAP);
  fragments already in range are left untouched and immutable.
- Wire repack into AdHocWithFilesCache.close() and the build_chunkindex_from_repo
  merge>1 branch (which no longer collapses-and-deletes everything).
- list_chunkindex_fragments() estimates a fragment's entry count from its stored
  byte size, so fragments can be classified without loading them.

This bounds both fragment size and count, and keeps large fragments stable
(groundwork for a future writethrough cache on the index/ namespace, which an
all-in-one consolidation would invalidate for every client of the repo).

Also:

Make chunk index fragment partitioning deterministic

Sort the selected keys before partitioning them into fragments, so an
identical set of entries always produces an identical fragment set
(identical content hashes), no matter in which order the entries were
inserted into the hash table.

This makes writing/repacking idempotent and convergent across clients:
a fragment that already exists in the repo is not stored again, and no
differently-partitioned duplicates of the same entries can pile up
(previously, two clients repacking the same small fragments could each
produce a different - possibly sealed, thus never cleaned up - merged
fragment).

Sorting cost is negligible on the hot paths: incremental writes and
repacks sort ~100k-200k keys (~0.02s). Only full rewrites (compact,
slow rebuild) sort the whole index, where the sort is dwarfed by the
rest of the operation.

Also:

Don't fail an already-committed backup on repack error

The repack in ChunksMixin.close() runs after the archive and its
incremental chunk-index fragment are durably stored, so it is optional
maintenance. Catch a transient repack failure and warn instead of letting
it propagate out of close() and fail the whole command; the next run
repacks again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:33:49 +02:00
TW
d3a9799563
Merge pull request #9864 from mr-raj12/pack-not-found-exception
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
repository: raise PackNotFound when get_many hits a missing pack
2026-07-06 19:38:15 +02:00
Mrityunjay Raj
cbc94ae837 repository: raise PackNotFound when get_many hits a missing pack 2026-07-06 08:34:59 +05:30
TW
87bd65c054
Merge pull request #9863 from ThomasWaldmann/compact-use-size-better-master
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
compact: use size better
2026-07-05 22:04:39 +02:00
Thomas Waldmann
230f6ef400
compact: only output statistics when --stats is given 2026-07-05 21:11:35 +02:00
Thomas Waldmann
2b2ceaeb2c
compact: show deduplication and compression factors, fixes #9856 2026-07-05 20:14:51 +02:00
TW
10df5a898b
Merge pull request #9862 from PhrozenByte/drop-symlink-mount-warning-master
Drop borgfs runtime warning about symlinks and improve corresponding docs [master]
2026-07-05 19:45:47 +02:00
Daniel Rudolf
88e186e053
Improve docs note about symlink behaviour 2026-07-05 18:11:12 +02:00
Daniel Rudolf
a56eb9e1db
Drop non-actionable borgfs warning about symlinks 2026-07-05 18:09:28 +02:00
TW
fc8c8e0fe9
Merge pull request #9811 from ThomasWaldmann/env-hostname-username-master
Replace create --hostname/--username with BORG_HOSTNAME/BORG_USERNAME env vars, fixes #9651
2026-07-05 14:56:52 +02:00
TW
08f91d4d62
Merge pull request #9860 from mvanhorn/docs/fix-contruct-typo
docs: fix typo in serve comment (contruct -> construct)
2026-07-05 14:55:21 +02:00
Charmi Kadi
cef78c99aa
json: support BORG_JSON_INDENT env var for JSON output formatting 2026-07-05 14:45:59 +02:00
Matt Van Horn
f59258e305
docs: fix typo in serve comment (contruct -> construct) 2026-07-05 04:26:48 -07:00
TW
caf1fe55fe
Merge pull request #9836 from mr-raj12/pack-files-get-many-grouping
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
repository: cache whole packs in get_many, replacing adjacent-read grouping
2026-07-05 08:24:49 +02:00
Mrityunjay Raj
cc098ecd5f repository: use try/except/else in get_many, fix misleading comment 2026-07-05 06:41:24 +05:30
TW
1926368ff9
Merge pull request #9857 from borgbackup/ai-findings-autofix/src-borg-testsuite-archiver-extract_cmd_test.py
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
extract tests: add missing comma between argument strings
2026-07-05 00:29:57 +02:00
TW
779b57e322
Merge pull request #9858 from borgbackup/ai-findings-autofix/src-borg-testsuite-helpers-parseformat_test.py
fix typo: zettabytes
2026-07-05 00:29:02 +02:00
Mrityunjay Raj
2e2ef6f47a repository: peek the pack cache in get(), switch to peekable helpers LRUCache 2026-07-04 22:14:55 +05:30
Mrityunjay Raj
ec62e5884c repository: use functools.lru_cache for the get_many pack cache 2026-07-04 22:14:55 +05:30
Mrityunjay Raj
f6712364df repository: cache whole packs in get_many, replacing adjacent-read grouping
Keep up to PACK_READER_CACHE_SIZE whole packs in an LRU so repeated and out-of-order ids read from memory instead of refetching from the store.
2026-07-04 22:14:55 +05:30
Mrityunjay Raj
d076788dc5 repository_test: cover get_many pack-grouping (one load per pack, order, missing ids) 2026-07-04 22:14:55 +05:30
Mrityunjay Raj
5d557e1bb6 repository: group adjacent same-pack reads in get_many into one store.load 2026-07-04 22:14:55 +05:30
TW
aba4e2dcf4
Apply suggested fix to src/borg/testsuite/helpers/parseformat_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:39:43 +02:00
TW
811175f0d1
Apply suggested fix to src/borg/testsuite/archiver/extract_cmd_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:37:14 +02:00
TW
cf0968ac82
Apply suggested fix to src/borg/testsuite/archiver/extract_cmd_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:37:14 +02:00
TW
6529346861
Apply suggested fix to src/borg/testsuite/archiver/extract_cmd_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:37:13 +02:00
TW
14f96bc0b8
Apply suggested fix to src/borg/testsuite/archiver/extract_cmd_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:37:13 +02:00
TW
196625f7c4
Apply suggested fix to src/borg/testsuite/archiver/extract_cmd_test.py from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 12:37:12 +02:00
TW
985cbc4178
Merge pull request #9853 from ThomasWaldmann/fix-create-enospc-atomicity
Some checks failed
Lint / lint (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / security (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
CI / asan_ubsan (push) Has been cancelled
CI / native_tests (push) Has been cancelled
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Has been cancelled
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Has been cancelled
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Has been cancelled
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Has been cancelled
CI / windows_tests (push) Has been cancelled
create: do not wrap repository writes in backup_io("read") (silent data loss on ENOSPC)
2026-07-03 20:35:37 +02:00
Thomas Waldmann
5aaa2ddc4b
create: don't wrap repository writes in backup_io("read")
process_file() ran process_file_chunks() inside `with backup_io("read")`.

That block was meant to guard reading the *source* file, but the source
reads are already guarded individually by backup_io_iter(). The outer
wrapper additionally caught add_chunk()'s *repository* writes, so a critical
repository IO failure -- e.g. the repo running out of space during a pack
flush -- was wrapped into a per-file BackupOSError. Borg then only warned,
skipped the file, and continued, and create_inner() still committed the
archive via archive.save().

The result: `borg create` on an out-of-space repo printed a normal success
summary ("Error files: 0"), exited 0, and committed an archive that
references chunks which were never durably stored. `borg check` afterwards
reports "Missing file chunk detected" and `borg compact` reports "Repository
has N missing objects!" -- silent, unrestorable-backup data loss.

Drop the outer backup_io("read") wrapper. Source reads stay per-file
warnings (backup_io_iter is unchanged); repository OSErrors are now left
unwrapped and therefore critical, aborting create before archive.save()
runs, exactly as the BackupOSError docstring prescribes.

Reproduced on a space-limited macOS ramdisk (source > free space): before,
create exited 0 with a corrupt archive; after, create fails and commits
nothing, and the repository stays consistent across a create/delete/compact
churn matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:48:35 +02:00
TW
98bf11363d
Merge pull request #9852 from mr-raj12/pack-defaults-compact-index-fix
size-bound 50 MB packs by default, fix stale chunk index after compact pack rewrites
2026-07-03 14:47:54 +02:00
Mrityunjay Raj
a8c2c7b1ec PackWriter: move pack size default to constants.py, keep it caller-set 2026-07-03 17:37:02 +05:30
TW
64d89c8d1b
Merge pull request #9851 from ThomasWaldmann/cache-close-resource-fixes
Release cache resources properly in Cache.close()
2026-07-03 14:04:18 +02:00
TW
f829eef49a
Merge pull request #9848 from mr-raj12/docs-pack-files-figures
docs: illustrate pack file format and sync packs.rst to multi-object packs
2026-07-03 14:03:35 +02:00
TW
e779e55278
Merge pull request #9847 from mr-raj12/fs-dir-subdirs
helpers/fs: let get_*_dir helpers take extra path components
2026-07-03 13:57:45 +02:00
Mrityunjay Raj
55535a21eb docs: address review, add excalidraw sources for pack figures
Fix outdated wording in pack figures and captions, document the
diagram workflow in development.rst, and check in editable sources.
2026-07-03 17:22:36 +05:30
Mrityunjay Raj
1930c0cead compact: fix chunk index going stale when packs are rewritten, fixes #9850
compact_pack() updated repository.chunks, but compact computes keep/drop from
and persists its own index, which kept pointing at the deleted packs. Also:
tolerate deleting already-gone packs, start rebuilt index entries as unused.
2026-07-03 16:52:32 +05:30
Mrityunjay Raj
dd1bb247a4 PackWriter: size-bound packs of 50 MB by default, fixes #9843
max_count=3 was a testing value. The default is now max_count=None,
max_size=50_000_000; the testsuite sets BORG_PACK_MAX_COUNT=3 instead,
so small test data still produces multiple multi-object packs.
2026-07-03 16:52:07 +05:30
Mrityunjay Raj
09a1627860 repository/compact: use get_cache_dir subdirs arg, pathlib for store cache dir 2026-07-03 15:55:27 +05:30
Thomas Waldmann
a200591bb4
Release files cache dict in Cache.close()
close() persists the files cache but never drops its reference to
self._files, unlike self._chunks which is cleared right after being
saved. For a backup of many files this dict can hold hundreds of MB and
stays alive for as long as the closed cache object does.

Set self._files = None after writing it, mirroring the _chunks handling.
Accessing .files after close() was never supported anyway (close() also
tears down cache_config, which _read_files_cache needs), so this only
frees memory earlier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:59:36 +02:00
Thomas Waldmann
9f5c6c5b73
Invalidate repository chunk index after clearing it in Cache.close()
AdHocWithFilesCache._chunks is the very same ChunkIndex object the
Repository holds (bound via the .chunks property). On close() we persist
it and clear the table in place (_maybe_write_chunks_cache(clear=True)),
then set self._chunks = None. But that only drops the *cache's* reference:
the Repository still points at the now-empty table and
is_chunk_index_loaded stays True.

A later access to repository.chunks in the same process (e.g.
delete_object doing self.chunks.get(id)) would then see a valid-looking
but empty index and conclude every chunk is gone, instead of rebuilding
from the repository. delete_chunkindex_from_repo() already guards its
analogous case with invalidate_chunk_index(); do the same here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:59:07 +02:00
TW
3d56f5a7c4
Merge pull request #9844 from ThomasWaldmann/update-changes-master
Some checks are pending
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
update CHANGES
2026-07-03 09:44:21 +02:00
Thomas Waldmann
b220fba833
update CHANGES 2026-07-03 09:43:41 +02:00
Mrityunjay Raj
8a24775180 docs: illustrate pack file format and sync packs.rst to multi-object packs, refs #8572 2026-07-03 10:40:36 +05:30
Mrityunjay Raj
db89931645 helpers/fs: let get_*_dir helpers take extra path components
get_config/cache/data/runtime_dir now accept *subdirs and ensure_dir the
full path. get_keys_dir and get_security_dir use this instead of
hand-joining "keys"/"security", and the store cache in repository.py uses
get_cache_dir("storecache") instead of its own makedirs.
2026-07-03 08:01:11 +05:30
TW
15d55d48e3
Merge pull request #9845 from ThomasWaldmann/rest-perf-test
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
borg2 performance harness for rest:// repositories
2026-07-02 21:33:28 +02:00