git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: BUG: Race condition due to reflog expiry in "gc"

From
Jeff King <peff@peff.net>
Date
Mar 13, 2019, 16:02 UTC
Message-ID
<20190313160204.GD24101@sigill.intra.peff.net>
In-Reply-To
<87sgvrax0o.fsf@evledraar.gmail.com>
On Wed, Mar 13, 2019 at 11:28:39AM +0100, Ævar Arnfjörð Bjarmason wrote:
> I was under the impression that git-gc was supposed to support operating
> on a repository that's concurrently being modified, as long as you don't
> set the likes of gc.pruneExpire too aggressively.

To some degree. If it has to take locks to modify items, then inherently there's going to be lock contention. But we may be able to work around it some.

A big one we used to hit at GitHub is that running `git pack-refs` needs to take the individual ref locks when pruning loose refs that have just been packed. We dealt with that by adding retry-with-timeout logic to ref lock acquisition, in 4ff0f01cb7 (refs: retry acquiring reference locks for 100ms, 2017-08-21). Since then, I can't remember seeing a single instance of this coming up in production use.

I don't think we use a timeout with the reflog lock. Maybe we ought to. It might need to be longer than the 100ms default for refs, since I think we'd do more significant work during the reflog expiration. On the other hand, I think we hold the lock on the ref itself, too, during that expiration.

We don't actually expire reflogs regularly at GitHub, so I can't say one way or the other if there would be lock contention there.

Show 8 quoted lines
> Running a "gc" in a loop without "git reflog expire --all" and when
> watching the repository being GC'd with:
> 
>     fswatch -l 0.1 -t -r . 2>&1 | grep lock
> 
> We only create .git/MERGE_RR.lock, .git/gc.pid.lock and
> git/packed-refs.lock. These are all things that would only cause another
> concurrent GC to fail, not a normal git command.

The packed-refs.lock can conflict with a normal operation; regular writers need to update it when they delete a ref.

You'd also be locking regular refs as part of "pack-refs --prune", but you probably don't see it running gc in a loop, because all of the loose refs are pruned after the first run.

> I'm just including that as illustration that add_reflogs_to_pending() in
> revision.c during "gc" already iterates over the reflogs without locking
> anything, but of course it's just reading them.

Right. It's always safe to read without locking (refs, packed-refs, and reflogs).

Show 19 quoted lines
> So just this fixes that:
> 
>     diff --git a/refs/files-backend.c b/refs/files-backend.c
>     index ef053f716c..b6576f28b9 100644
>     --- a/refs/files-backend.c
>     +++ b/refs/files-backend.c
>     @@ -3037,7 +3037,7 @@ static int files_reflog_expire(struct ref_store *ref_store,
>      	 * reference itself, plus we might need to update the
>      	 * reference if --updateref was specified:
>      	 */
>     -	lock = lock_ref_oid_basic(refs, refname, oid,
>     +	lock = lock_ref_oid_basic(refs, refname, NULL,
>      				  NULL, NULL, REF_NO_DEREF,
>      				  &type, &err);
>      	if (!lock) {
> 
> Which seems sensible to me. We'll still get the lock, we just don't
> assert that the refname we use to get the lock must be at that
> SHA-1. We'll still use it for the purposes of expiry.

I _think_ this should be OK, because we don't open the reflog until after we hold the lock. So we don't really care what the value was at. Wherever we got the lock, we'll do the expiration process atomically at that point.

I don't think this makes all your problems go away, though, because you'd still have the immediate contention of actually taking the lock (at any value).

> But maybe I've missed some caveat in reflog_expiry_prepare() and friends
> and we really do need the reflog at that OID, then this would suck less:

If I'm reading the code correctly, we don't call reflog_expiry_prepare() until we're holding the lock (because we pass it in as a callback to reflog_expire()). So I think that would be OK.

Show 12 quoted lines
>      	if (!lock) {
>     +		if (errno == EBUSY) {
>     +			warning("cannot lock ref '%s': %s. Skipping!", refname, err.buf);
>     +			strbuf_release(&err);
>     +			return -2;
>     +		}
>      		error("cannot lock ref '%s': %s", refname, err.buf);
>      		strbuf_release(&err);
>      		return -1;
> 
> I.e. we just detect the EBUSY that verify_lock() sets if the OID doesn't
> match, and don't prune that reflog.

That saves git-gc from failing. But do we have the problem in the other direction? I.e., that the gc would take a lock, and the actual user request to update a ref would fail?

That's what the retry-with-timeout is supposed to address, so maybe it works. But I wouldn't be surprised if it's insufficient in practice, since the reflog code may walk big parts of the graph under lock, checking reachability.

-Peff
Previous: Ævar Arnfjörð BjarmasonNext: Ævar Arnfjörð Bjarmason
Message 4 of 60 in “BUG: Race condition due to reflog expiry in "gc"”
  1. Ævar Arnfjörð BjarmasonMar 12, 2019
  2. Junio C HamanoMar 13, 2019
  3. Ævar Arnfjörð BjarmasonMar 13, 2019
  4. Jeff KingMar 13, 2019
  5. Ævar Arnfjörð BjarmasonMar 13, 2019
  6. 0/5 gc: minor code cleanup + contention fixesÆvar Arnfjörð Bjarmason, Mar 13, 2019
  7. 0/7 gc: minor code cleanup + contention fixesÆvar Arnfjörð Bjarmason, Mar 14, 2019
  8. 0/8 gc: minor code cleanup + contention fixesÆvar Arnfjörð Bjarmason, Mar 15, 2019
  9. Junio C HamanoMar 18, 2019
  10. 0/7 gc: tests and handle reflog expire configÆvar Arnfjörð Bjarmason, Mar 28, 2019
  11. 1/7 gc: remove redundant check for gc_auto_thresholdÆvar Arnfjörð Bjarmason, Mar 28, 2019
  12. 2/7 gc: convert to using the_hash_algoÆvar Arnfjörð Bjarmason, Mar 28, 2019
  13. 3/7 gc: refactor a "call me once" patternÆvar Arnfjörð Bjarmason, Mar 28, 2019
  14. 5/7 reflog tests: test for the "points nowhere" warningÆvar Arnfjörð Bjarmason, Mar 28, 2019
  15. 4/7 reflog tests: make use of "test_config" idiomÆvar Arnfjörð Bjarmason, Mar 28, 2019
  16. 7/7 gc: handle & check gc.reflogExpire configÆvar Arnfjörð Bjarmason, Mar 28, 2019
  17. 6/7 reflog tests: assert lack of early exit with expiry="never"Ævar Arnfjörð Bjarmason, Mar 28, 2019
  18. 1/8 gc: remove redundant check for gc_auto_thresholdÆvar Arnfjörð Bjarmason, Mar 15, 2019
  19. 2/8 gc: convert to using the_hash_algoÆvar Arnfjörð Bjarmason, Mar 15, 2019
  20. 3/8 gc: refactor a "call me once" patternÆvar Arnfjörð Bjarmason, Mar 15, 2019
  21. 4/8 reflog tests: make use of "test_config" idiomÆvar Arnfjörð Bjarmason, Mar 15, 2019
  22. 5/8 reflog tests: test for the "points nowhere" warningÆvar Arnfjörð Bjarmason, Mar 15, 2019
  23. 6/8 reflog tests: assert lack of early exit with expiry="never"Ævar Arnfjörð Bjarmason, Mar 15, 2019
  24. Jeff KingMar 19, 2019
  25. 7/8 gc: handle & check gc.reflogExpire configÆvar Arnfjörð Bjarmason, Mar 15, 2019
  26. 8/8 reflog expire: don't assert the OID when locking refsÆvar Arnfjörð Bjarmason, Mar 15, 2019
  27. Jeff KingMar 19, 2019
  28. 1/7 gc: remove redundant check for gc_auto_thresholdÆvar Arnfjörð Bjarmason, Mar 14, 2019
  29. 2/7 gc: convert to using the_hash_algoÆvar Arnfjörð Bjarmason, Mar 14, 2019
  30. Duy NguyenMar 15, 2019
  31. Ævar Arnfjörð BjarmasonMar 15, 2019
  32. Johannes SchindelinMar 15, 2019
  33. 3/7 gc: refactor a "call me once" patternÆvar Arnfjörð Bjarmason, Mar 14, 2019
  34. 4/7 reflog tests: make use of "test_config" idiomÆvar Arnfjörð Bjarmason, Mar 14, 2019
  35. 5/7 reflog: exit early if there's no work to doÆvar Arnfjörð Bjarmason, Mar 14, 2019
  36. Duy NguyenMar 15, 2019
  37. Ævar Arnfjörð BjarmasonMar 15, 2019
  38. 7/7 reflog expire: don't assert the OID when locking refsÆvar Arnfjörð Bjarmason, Mar 14, 2019
  39. Duy NguyenMar 15, 2019
  40. Ævar Arnfjörð BjarmasonMar 15, 2019
  41. 6/7 gc: don't run "reflog expire" when keeping reflogsÆvar Arnfjörð Bjarmason, Mar 14, 2019
  42. Duy NguyenMar 15, 2019
  43. Ævar Arnfjörð BjarmasonMar 15, 2019
  44. Duy NguyenMar 15, 2019
  45. Johannes SchindelinMar 15, 2019
  46. Junio C HamanoMar 18, 2019
  47. 1/5 gc: remove redundant check for gc_auto_thresholdÆvar Arnfjörð Bjarmason, Mar 13, 2019
  48. Jeff KingMar 14, 2019
  49. 2/5 gc: convert to using the_hash_algoÆvar Arnfjörð Bjarmason, Mar 13, 2019
  50. Jeff KingMar 14, 2019
  51. 3/5 gc: refactor a "call me once" patternÆvar Arnfjörð Bjarmason, Mar 13, 2019
  52. Jeff KingMar 14, 2019
  53. 4/5 gc: don't run "reflog expire" when keeping reflogsÆvar Arnfjörð Bjarmason, Mar 13, 2019
  54. Jeff KingMar 14, 2019
  55. Junio C HamanoMar 14, 2019
  56. Jeff KingMar 14, 2019
  57. 5/5 reflog expire: don't assert the OID when locking refsÆvar Arnfjörð Bjarmason, Mar 13, 2019
  58. Jeff KingMar 14, 2019
  59. Jeff KingMar 14, 2019
  60. Ævar Arnfjörð BjarmasonMar 21, 2019

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.