repository: cache whole packs in get_many, replacing adjacent-read grouping#9836
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9836 +/- ##
==========================================
- Coverage 85.11% 85.08% -0.03%
==========================================
Files 93 93
Lines 15408 15436 +28
Branches 2326 2332 +6
==========================================
+ Hits 13114 13134 +20
- Misses 1596 1603 +7
- Partials 698 699 +1 ☔ View full report in Codecov by Harness. |
|
Hmm, if an items chunks list is |
https://gist.github.com/mr-raj12/830ebd466f3349739c534556a4486c5c |
|
the same id repeats in a get_many call, we end up reading it from the store again instead of reusing what we already have. Should I handle this dedup (with a proper memory limit) in this PR only, or would you prefer a separate follow-up? |
|
Well, maybe i shouldn't have asked "does this work", but rather said "guess this is not how it should work". Reading a pack from the store should go through a small LRU-cache of PackReaders. A PackReader instance could have a mapping property that scans the pack on first access and remembers the mapping get_many should always try to get an object from a cached PackReader first, before requesting it from the store. |
|
BTW, just saw this on twitter and found it funny: |
literally me hanging on the wall 🥇 |
003d5f7 to
e8ed192
Compare
ThomasWaldmann
left a comment
There was a problem hiding this comment.
use the force! ehrm, the python stdlib. or borg.helpers.
e8ed192 to
1199291
Compare
ThomasWaldmann
left a comment
There was a problem hiding this comment.
Looks like this works!
But maybe we can even improve it further...
…ouping 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.
cd7cc04 to
2e2ef6f
Compare
ThomasWaldmann
left a comment
There was a problem hiding this comment.
i only reviewed the repository code this time.
| if raise_missing: | ||
| raise self.ObjectNotFound(id_, str(self._location)) from None |
There was a problem hiding this comment.
if get() does not find an OBJECT id in the index, it will raise ObjectNotFound (correct). this means we do not have it in the index and likely also not in a pack and there is no sign of index corruption.
if get_many() does not find a PACK, it raises the same exception, although this points to a bigger issue: we have an index entry that points to a pack, but that whole pack is missing, not just a single object. this either means the index is wrong or we have lost more data than a single object.
guess we need a different exception here, separate PR.

What
Repository.get_manycalledstore.loadonce per object. Objects in a pack file are stored back-to-back and a pack is immutable once written, soget_manynow fetches each pack once and slices every requested object out of the in-memory copy. Repeated ids and reads that interleave several packs are served from memory instead of re-fetching.How
For each requested id,
get_manylooks up the chunks index entry (pack_id,obj_offset,obj_size). The pack is loaded whole on a cache miss and kept in a per-repositoryborg.helpers.LRUCacheof up toPACK_READER_CACHE_SIZE(3) packs, keyed bypack_id. Later ids whose pack is still cached are sliced from memory with no store request. The cache is cleared when the repository is closed.get()peeks the same cache before doing a store range request, in all cases includingread_data=False: if the object's pack is already cached it is sliced from memory, otherwiseget()does ranged reads as before and never loads a whole pack itself. Ids with no index entry, or still buffered in the pack writer, delegate fromget_manytoget().Why it matters
On backends with per-call latency (S3, SFTP, any network store),
Archive.fetch_many->Repository.get_manydominates extract time. Deduplication makes the same chunk id repeat: a sparse file writes many identical zero chunks in a row, and a block that recurs later reappears scattered through the list. Items also interleave objects from several packs. Caching whole packs collapses this to onestore.loadper distinct pack, regardless of request order or repeats. The earlier adjacent-run grouping only helped when the request order matched storage order with no repeats.Tests
Tests in
repository_test.py:test_get_many_one_load_per_pack:store.stats["load_calls"]delta equals the number of packs, not the number of objects.test_get_many_keeps_request_order: ids interleaved across two packs load each pack once and come back in request order.test_get_many_repeated_ids: a deduplicated list like[A, A, B, C, B]returns the right bytes at every position with one load per distinct pack.test_get_many_evicts_least_recently_used: visiting more packs than the cache holds evicts the oldest, and revisiting an evicted pack reloads it.test_get_many_missing_id_yields_none: withraise_missing=False, a missing id yieldsNonein its position without disturbing neighbors.test_get_reuses_cached_pack: a coldget()does one ranged read and does not populate the cache; afterget_manycaches the pack,get()andget(read_data=False)do zero store loads.