Skip to content

Commit a912b8c

Browse files
committed
packfile: recover when a multi-pack-index names a removed pack
When a geometric repack runs concurrently with other git processes, it can write a new pack and multi-pack-index and then delete older packs that the new one subsumes. One or more of those older packs may have been indexed by the previous multi-pack-index. A process that already had the previous multi-pack-index open keeps using it, and that stale index still records the removed pack(s) as owning some objects. Because a multi-pack-index attributes each object to exactly one pack, an object that exists in multiple covered packs is served only through its recorded owner. If that owner is the pack a concurrent repack just removed, find_pack_entry() cannot serve the object: fill_midx_entry() routes the lookup to the missing pack (prepare_midx_pack() fails), and the regular pack fallback deliberately skips every multi-pack-index covered pack. The object is reported missing even though a perfectly good copy survives in another covered pack -- for example a large "base" pack that geometric repacking intentionally kept. The false negative is not limited to one caller. Any reader (cat-file, rev-list, pack-objects, ...) can spuriously fail with "unable to read object", and callers that only ask whether an object exists get a wrong answer too, since the OBJECT_INFO_QUICK path never retries. Writers that merge in-core, such as "git replay", are hit hardest: merge-ort treats the unreadable tree as a premature abort, sets result.clean < 0, and returns without a result tree. Teach find_pack_entry() to recover. After the normal multi-pack-index lookup and the regular pack fallback both miss, check whether the object is nonetheless present in a covered multi-pack-index (bsearch_midx()). If it is, its recorded owner must have become unavailable, so scan that index's packs directly for a surviving copy. The bsearch gate keeps genuine misses (i.e. objects absent from the index) on the fast path, and because the recovery lives in find_pack_entry() itself it also fixes the OBJECT_INFO_QUICK callers that never reprepare. This recovers the object without touching the multi-pack-index itself. Reloading the stale index would be a more complete fix but would be much more involved: other code (pack bitmaps, object name disambiguation) borrows and caches the "struct multi_pack_index *" across object reads, so freeing it underneath them would be a use-after-free. Refreshing the index with proper invalidation of those borrowers is left for future work. Signed-off-by: Elijah Newren <newren@gmail.com>
1 parent 6969db9 commit a912b8c

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

‎odb/source-packed.c‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ static int find_pack_entry(struct odb_source_packed *store,
3131
}
3232
}
3333

34+
/*
35+
* Recovery for a concurrent-repack race: a MIDX can name an owning
36+
* pack for an object that a simultaneous repack has since deleted,
37+
* even though the object still exists in another pack the same MIDX
38+
* covers (e.g. a kept base pack that geometric repack did not rewrite).
39+
* If the object is present in a MIDX yet none of the paths above could
40+
* serve it, its recorded owning pack has become unavailable. The
41+
* regular fallback above deliberately skips MIDX-covered packs, so
42+
* scan this MIDX's packs directly to find the surviving copy. The
43+
* bsearch gate keeps genuine misses (objects absent from the MIDX) on
44+
* the fast path.
45+
*/
46+
if (store->midx) {
47+
struct multi_pack_index *m = store->midx;
48+
uint32_t midx_pos, i;
49+
50+
if (bsearch_midx(oid, m, &midx_pos)) {
51+
for (i = 0; i < m->num_packs + m->num_packs_in_base; i++) {
52+
struct packed_git *p;
53+
54+
if (prepare_midx_pack(m, i))
55+
continue;
56+
p = nth_midxed_pack(m, i);
57+
if (p && packfile_fill_entry(p, oid, e))
58+
return 1;
59+
}
60+
}
61+
}
62+
3463
return 0;
3564
}
3665

‎t/t5319-multi-pack-index.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,4 +1393,44 @@ test_expect_success 'pack.preferBitmapTips interprets patterns as hierarchy' '
13931393
)
13941394
'
13951395

1396+
test_expect_success 'lookup recovers object whose midx-owning pack was removed' '
1397+
test_when_finished "rm -fr repo" &&
1398+
git init repo &&
1399+
(
1400+
cd repo &&
1401+
1402+
# "keep" ends up only in the big pack; "dup" is deliberately
1403+
# placed in two packs so the midx has to choose an owner.
1404+
test_commit keep &&
1405+
echo duplicated-content >dup &&
1406+
git add dup &&
1407+
git commit -m dup &&
1408+
dup_oid=$(git rev-parse HEAD:dup) &&
1409+
1410+
# Roll every object, including dup, into a single big pack.
1411+
git repack -adq &&
1412+
1413+
# Build a second, "moderate" pack that also contains dup, so dup
1414+
# now lives in two packs that the midx will cover.
1415+
moderate=$(echo "$dup_oid" |
1416+
git pack-objects --quiet $objdir/pack/pack) &&
1417+
1418+
# Attribute dup to the moderate pack in the midx.
1419+
git multi-pack-index write \
1420+
--preferred-pack="pack-$moderate.idx" &&
1421+
1422+
# Simulate a concurrent "git repack" retiring the moderate pack:
1423+
# its files disappear, but the now-stale midx still names it as
1424+
# the owner of dup. A valid copy of dup survives in the big pack.
1425+
rm -f $objdir/pack/pack-$moderate.* &&
1426+
1427+
# The midx routes the lookup to the deleted pack, and the regular
1428+
# pack fallback skips midx-covered packs, so without recovery dup
1429+
# would appear missing even though it is physically present.
1430+
echo blob >expect &&
1431+
git cat-file -t "$dup_oid" >actual &&
1432+
test_cmp expect actual
1433+
)
1434+
'
1435+
13961436
test_done

0 commit comments

Comments
 (0)