From: Derrick Stolee <stolee@gmail.com>
To: Elijah Newren via GitGitGadget <gitgitgadget@gmail.com>,
git@vger.kernel.org
Cc: Patrick Steinhardt <ps@pks.im>, Elijah Newren <newren@gmail.com>,
Jeff King <peff@peff.net>
Subject: Re: [PATCH v3 4/4] packfile: recover when a multi-pack-index names a removed pack
Date: Tue, 1 Sep 2026 11:26:25 -0400 [thread overview]
Message-ID: <944945ab-dde7-41e5-af92-fc520485fc53@gmail.com> (raw)
In-Reply-To: <9b0966df9a060df215d8aec7816875d42651d5bb.1787986831.git.gitgitgadget@gmail.com>
On 8/29/2026 3:00 AM, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
I'm late in reviewing this patch, so forgive me responding inline as
I discover how it works.
tl;dr: Good patch. LGTM.
> Teach find_pack_entry() to recover. The MIDX lookup now returns a
> tri-state, distinguishing an object absent from the MIDX from one it owns
> via a pack that can no longer be opened; in the latter case, once the
> regular fallback has also missed, scan the MIDX's packs directly for a
> surviving copy. Because the return value is no longer a boolean, rename
> fill_midx_entry() to midx_fill_entry() so callers must reckon with the
> new enum rather than silently treat MIDX_FILL_OWNER_UNAVAILABLE as a hit.
This tri-state is valuable!
> Do the scan only on the second read (OBJECT_INFO_SECOND_READ): by then
> the cheaper on-disk reload has run, so an object merely relocated into a
> new (uncovered) pack has already been found by the regular fallback, and
> only a genuine hidden duplicate reaches the rescan. A QUICK caller that
> skips the second read simply accepts the false negative, as QUICK is
> designed to.
>
> Reloading the stale MIDX would be a more complete fix but is much more
> involved (the borrowers above need proper invalidation), so leave that
> for later.
> - if (m && fill_midx_entry(m, oid, &e, NULL)) {
> + if (m && midx_fill_entry(m, oid, &e, NULL) == MIDX_FILL_HIT) {
One major benefit to the rename is that we can guarantee that
all callers are updated to reflect the new tri-state response.
It also has a better naming convention, overall.
(reordered header file diff up)
> +/*
> + * Result of looking an object up in a multi-pack-index. MIDX_FILL_HIT means
> + * "e was filled in"; the two miss variants distinguish an object the midx does
> + * not know about (MIDX_FILL_MISS) from one it does know about but whose owning
> + * pack we can no longer open (MIDX_FILL_OWNER_UNAVAILABLE -- the signature of a
> + * concurrent repack having removed that pack). A known-bad (corrupt) object
> + * reports MIDX_FILL_MISS but also sets *bad_pack, if provided, to the owning
> + * pack so the caller can tell "corrupt" apart from "absent".
> + */
> +enum midx_fill_result {
> + MIDX_FILL_MISS = 0,
> + MIDX_FILL_HIT,
> + MIDX_FILL_OWNER_UNAVAILABLE,
> +};
> +
> +enum midx_fill_result midx_fill_entry(struct multi_pack_index *m,
> + const struct object_id *oid,
> + struct pack_entry *e,
> + struct packed_git **bad_pack);
This is good documentation that will help future uses know how to
react to the different modes.
> -int fill_midx_entry(struct multi_pack_index *m,
> - const struct object_id *oid,
> - struct pack_entry *e,
> - struct packed_git **bad_pack)
> +enum midx_fill_result midx_fill_entry(struct multi_pack_index *m,
> + const struct object_id *oid,
> + struct pack_entry *e,
> + struct packed_git **bad_pack)
> {
> uint32_t pos;
> uint32_t pack_int_id;
> struct packed_git *p;
>
> if (!bsearch_midx(oid, m, &pos))
> - return 0;
> + return MIDX_FILL_MISS;
Obviously correct: this OID isn't in the sorted list.
> midx_for_object(&m, pos);
> pack_int_id = nth_midxed_pack_int_id(m, pos);
>
> if (prepare_midx_pack(m, pack_int_id))
> - return 0;
> + return MIDX_FILL_OWNER_UNAVAILABLE;
Obviously correct: we tried to open the pack index but failed.
> p = m->packs[pack_int_id - m->num_packs_in_base];
>
> /*
> @@ -616,19 +616,19 @@ int fill_midx_entry(struct multi_pack_index *m,
> * loaded!
> */
> if (!is_pack_valid(p))
> - return 0;
> + return MIDX_FILL_OWNER_UNAVAILABLE;
Same: Pack is invalid somehow, likely that the .pack disappeared.
> if (oidset_size(&p->bad_objects) &&
> oidset_contains(&p->bad_objects, oid)) {
> if (bad_pack && !*bad_pack)
> *bad_pack = p;
> - return 0;
> + return MIDX_FILL_MISS;
This one is tricky, but makes sense: we have marked this as a
"bad" object so we should act like it doesn't exist. Good.
> }
>
> e->offset = nth_midxed_offset(m, pos);
> e->p = p;
>
> - return 1;
> + return MIDX_FILL_HIT;
finally: success!> }
> static int find_pack_entry(struct odb_source_packed *store,
> const struct object_id *oid,
> struct pack_entry *e,
> + enum object_info_flags flags,
> struct packed_git **bad_pack)
> {
> struct packfile_list_entry *l;
> + enum midx_fill_result midx_result = MIDX_FILL_MISS;
>
> odb_source_prepare(&store->base, 0);
> - if (store->midx && fill_midx_entry(store->midx, oid, e, bad_pack))
> - return 1;
> + if (store->midx) {
> + midx_result = midx_fill_entry(store->midx, oid, e, bad_pack);
> + if (midx_result == MIDX_FILL_HIT)
> + return 1;
> + }
This looks good. On a hit, we return. Act like a MIDX-miss if we
don't have a midx.
Outside of the patch context is the "reprepare packfiles" to pick
up a copy from a packfile that doesn't exist within the current
(stale) midx.
> + /*
> + * Recovery for a concurrent-repack race: a stale MIDX may still name a
> + * vanished owning pack even though the object survives in another pack
> + * the same MIDX covers. The regular fallback above skips MIDX-covered
> + * packs, and repreparing the on-disk pack set does not reload the
> + * borrowed, cached MIDX, so scan its packs directly for the survivor.
> + *
> + * Do this only on the second read, by which point repreparing packs has
> + * already had a chance to find an object merely relocated into a new,
> + * uncovered pack; only a genuine hidden duplicate reaches here.
> + */
This comment does a lot of important context-setting to show
that we are in a very narrow case: the stale MIDX has multiple
packs that contain the requested object, but the "newer" one
was deleted without creating a new packfile, so we need to
look at each contained pack for the object from its pack-index.
> + if (midx_result == MIDX_FILL_OWNER_UNAVAILABLE &&
> + (flags & OBJECT_INFO_SECOND_READ)) {
> + struct multi_pack_index *m = store->midx;
> + uint32_t i;
> +
> + for (i = 0; i < m->num_packs + m->num_packs_in_base; i++) {
> + struct packed_git *p;
> +
> + if (prepare_midx_pack(m, i))
> + continue;
> + p = nth_midxed_pack(m, i);
> + if (p && packfile_fill_entry(p, oid, e, bad_pack))
> + return 1;
> + }
> + }
> +
This is hopefully a very rare case, but it's good to have
this "fall back to O(num packs)" situation.
> +test_expect_success 'lookup recovers object whose midx-owning pack was removed' '
> + test_when_finished "rm -fr repo" &&
> + git init repo &&
> + (
> + cd repo &&
> +
> + # "keep" ends up only in the big pack; "dup" is deliberately
> + # placed in two packs so the midx has to choose an owner.
> + test_commit keep &&
> + echo duplicated-content >dup &&
> + git add dup &&
> + git commit -m dup &&
> + dup_oid=$(git rev-parse HEAD:dup) &&
> +
> + # Roll every object, including dup, into a single big pack.
> + git repack -adq &&
> +
> + # Build a second, "moderate" pack that also contains dup, so dup
> + # now lives in two packs that the midx will cover.
> + moderate=$(echo "$dup_oid" |
> + git pack-objects --quiet $objdir/pack/pack) &&
> +
> + # Attribute dup to the moderate pack in the midx.
> + git multi-pack-index write \
> + --preferred-pack="pack-$moderate.idx" &&
This use of preferred pack is a good way of getting around mtimes
that could be equal. We could also consider updating mtimes, but
this works so don't change it.
> + # Simulate a concurrent "git repack" retiring the moderate pack:
> + # its files disappear, but the now-stale midx still names it as
> + # the owner of dup. A valid copy of dup survives in the big pack.
> + rm -f $objdir/pack/pack-$moderate.* &&
> +
> + # The midx routes the lookup to the deleted pack, and the regular
> + # pack fallback skips midx-covered packs, so without recovery dup
> + # would appear missing even though it is physically present.
> + echo blob >expect &&
> + git cat-file -t "$dup_oid" >actual &&
> + test_cmp expect actual
> + )
> +'
Thanks for adding this test so we can keep this narrow case
working in perpetuity.
Thanks,
-Stolee
next prev parent reply other threads:[~2026-09-01 15:26 UTC|newest]
Thread overview: 49+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-18 22:34 [PATCH 0/2] Objects treated as missing despite being present, due to race with geometric repacking Elijah Newren via GitGitGadget
2026-08-18 22:34 ` [PATCH 1/2] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-19 18:09 ` Junio C Hamano
2026-08-21 1:44 ` Elijah Newren
2026-08-21 3:37 ` Junio C Hamano
2026-08-18 22:34 ` [PATCH 2/2] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-19 18:21 ` Junio C Hamano
2026-08-20 7:54 ` Patrick Steinhardt
2026-08-21 1:36 ` Elijah Newren
2026-08-24 4:48 ` Jeff King
2026-08-24 5:13 ` Patrick Steinhardt
2026-08-24 6:55 ` Jeff King
2026-08-24 7:06 ` Jeff King
2026-08-24 7:23 ` Jeff King
2026-08-25 7:38 ` Elijah Newren
2026-08-24 4:55 ` Jeff King
2026-08-24 5:40 ` Patrick Steinhardt
2026-08-24 7:03 ` Jeff King
2026-08-25 7:19 ` Elijah Newren
2026-08-24 14:45 ` Derrick Stolee
2026-08-25 7:38 ` Elijah Newren
2026-08-24 14:46 ` Derrick Stolee
2026-08-25 19:00 ` [PATCH v2 0/4] Objects treated as missing despite being present, due to race with geometric repacking Elijah Newren via GitGitGadget
2026-08-25 19:00 ` [PATCH v2 1/4] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-25 19:00 ` [PATCH v2 2/4] mktree: plug per-tree leak in --batch mode Elijah Newren via GitGitGadget
2026-08-27 5:36 ` Jeff King
2026-08-25 19:00 ` [PATCH v2 3/4] packfile: recover object lookups racing a concurrent repack Elijah Newren via GitGitGadget
2026-08-27 5:57 ` Jeff King
2026-08-27 22:23 ` Elijah Newren
2026-08-29 11:32 ` Jeff King
2026-08-25 19:00 ` [PATCH v2 4/4] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-27 6:06 ` Jeff King
2026-08-28 7:29 ` Elijah Newren
2026-08-29 11:34 ` Jeff King
2026-08-29 7:00 ` [PATCH v3 0/4] Objects treated as missing despite being present, due to race with geometric repacking Elijah Newren via GitGitGadget
2026-08-29 7:00 ` [PATCH v3 1/4] replay: fail gracefully when a merge input is unreadable Elijah Newren via GitGitGadget
2026-08-29 7:00 ` [PATCH v3 2/4] mktree: plug per-tree leak in --batch mode Elijah Newren via GitGitGadget
2026-08-29 7:00 ` [PATCH v3 3/4] mktree: do not use OBJECT_INFO_QUICK when checking objects Elijah Newren via GitGitGadget
2026-08-29 11:46 ` Jeff King
2026-08-29 7:00 ` [PATCH v3 4/4] packfile: recover when a multi-pack-index names a removed pack Elijah Newren via GitGitGadget
2026-08-29 12:07 ` Jeff King
2026-08-30 20:53 ` Junio C Hamano
2026-08-31 10:43 ` Patrick Steinhardt
2026-08-31 23:10 ` Jeff King
2026-09-01 15:27 ` Derrick Stolee
2026-09-01 16:04 ` Junio C Hamano
2026-09-01 15:26 ` Derrick Stolee [this message]
2026-09-01 16:47 ` Elijah Newren
2026-09-01 17:12 ` Derrick Stolee
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=944945ab-dde7-41e5-af92-fc520485fc53@gmail.com \
--to=stolee@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitgitgadget@gmail.com \
--cc=newren@gmail.com \
--cc=peff@peff.net \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox