Git development
 help / color / mirror / Atom feed
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


  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