Git development
 help / color / mirror / Atom feed
From: Jeff King <peff@peff.net>
To: Ted Nyman <tnyman@openai.com>
Cc: git@vger.kernel.org, gitster@pobox.com, me@ttaylorr.com,
	ps@pks.im, karthik.188@gmail.com, sandals@crustytoothpaste.net,
	avarab@gmail.com
Subject: Re: [PATCH v5 2/3] http: avoid concurrent appends to partial packs
Date: Sun, 26 Jul 2026 05:20:27 -0400	[thread overview]
Message-ID: <20260726092027.GA3529827@coredump.intra.peff.net> (raw)
In-Reply-To: <d9667c93b03d1a71df55a33f90538b31afd08677.1785047139.git.tnyman@openai.com>

On Sat, Jul 25, 2026 at 11:44:47PM -0700, Ted Nyman wrote:

> Pack requests stage downloads in a predictable partial-pack file so an
> interrupted transfer can be resumed. Both packfile URI and ordinary dumb
> HTTP requests use this staging path. Opening it in append mode forces
> each write to the current end of the file, so concurrent responses can
> append duplicate data and corrupt the pack.
> 
> Open the partial pack read-write without O_APPEND and seek once to its
> current end. Each downloader then retains the offset matching the Range
> it requested. Because the staging key must uniquely identify immutable
> pack contents, overlapping responses write the same bytes at the same
> offsets instead of extending the file with duplicate data.

OK. I still think this is kind of horrible and gross, but I can't think
of a reason it won't work (at least on POSIX-ish systems) and it solves
the problem with minimal changes and risk of regression.

I wondered about racing with another concurrent writer on the seek, but
I think it is OK. We seek immediately, and then use that offset (which
we get from another seek, replacing ftell()) as the value for our range
request. So even if somebody else advances the file, we have _some_
atomic value that we'll start writing to ourselves, and the worst case
is redundantly requesting a few bytes.

> MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an
> existing file. Create a missing partial pack exclusively, close it, and
> reopen it without O_CREAT so every retained descriptor permits another
> downloader to unlink the staging path. Duplicate that descriptor for
> index-pack instead of reopening the path after closing the stream;
> index-pack installs its own pack and the shared staging file is only
> unlinked, never renamed.

This part I have no real knowledge or opinion on the Windows bits (or if
there's an easier way to do it).

> Accept HTTP 416 when a partial pack is already
> complete and let index-pack validate its contents.

I wonder if we still need this or not. AIUI the original 416 responses
came because we were asking for nonsense outside of the range (because
the corrupted writes advanced the file too far). The worst case now is
that we'd ask for bytes "N-" when the file is only N bytes long, and the
server should say "OK, here are your 0 bytes". But maybe there's a
server who complains about that.

> diff --git a/http.c b/http.c
> index caccf2108e..a0d399b274 100644
> --- a/http.c
> +++ b/http.c
> @@ -2688,10 +2688,13 @@ int finish_http_pack_request(struct http_pack_request *preq)
>  	int tmpfile_fd;
>  	int ret = 0;
>  
> +	/* Another downloader may unlink the staging path while we index it. */
> +	tmpfile_fd = xdup(fileno(preq->packfile));
>  	fclose(preq->packfile);
>  	preq->packfile = NULL;
> -
> -	tmpfile_fd = xopen(preq->tmpfile.buf, O_RDONLY);
> +	if (lseek(tmpfile_fd, 0, SEEK_SET) < 0)
> +		die_errno("unable to seek local file %s for pack",
> +			  preq->tmpfile.buf);

OK, here we are avoiding the race that it gets unlinked by dup-ing the
existing descriptor and seeking back to the start. Makes sense. But
then...

> @@ -2704,13 +2707,8 @@ int finish_http_pack_request(struct http_pack_request *preq)
>  	else
>  		ip.no_stdout = 1;
>  
> -	if (run_command(&ip)) {
> +	if (run_command(&ip))
>  		ret = -1;
> -		goto cleanup;
> -	}
> -
> -cleanup:
> -	close(tmpfile_fd);
>  	unlink(preq->tmpfile.buf);
>  	return ret;

What is going on with this hunk? We don't really need to jump to cleanup
here because we get there directly anyway, and there are no other users
of the cleanup label. So that part doesn't seem wrong, but rather
unrelated.

More importantly, why don't we need to close tmpfile_fd anymore? We hand
it off to run_command(), which will always close it. So I _think_ it was
always wrong to close it ourselves here. If so, then could this hunk
become a preparatory commit on its own?

This commit is already confusing enough that the more extraneous stuff
we can take out of it the better.

> @@ -2738,22 +2736,45 @@ struct http_pack_request *new_http_pack_request(
> [...]
> +	/*
> +	 * MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an
> +	 * existing file; reopen a newly created file so others may unlink it.
> +	 */
> +	for (;;) {
> +		fd = open(preq->tmpfile.buf, O_RDWR);
> +		if (fd >= 0 || errno != ENOENT)
> +			break;
> +		fd = open(preq->tmpfile.buf, O_RDWR | O_CREAT | O_EXCL, 0666);
> +		if (fd >= 0) {
> +			close(fd);
> +			continue;
> +		}
> +		if (errno != EEXIST)
> +			break;
> +	}

OK, and this is the opening magic. What's going on with the O_EXCL here,
though? We try to open once, and if that fails with ENOENT then we open
again. But isn't that racy? Two processes simultaneously try to open(),
find the file is not there, and then both try O_EXCL. Only one of them
will win, and the other will barf.

I guess that is the reason for the loop, where we will try again over
and over until we either pick up somebody else's copy or get our own.
And if we get our own, we still close it and try again. And that's the
Windows magic described in the commit message.

That is...subtle as hell. I really wonder if it would be worth
introducing the basic form of this (just opening once with O_RDWR) and
then doing the Windows hackery on top as a separate commit. That would
leave the intermediate state subject to racy problems on Windows. But
when balancing bisectability versus having a clear human-readable patch,
I think I'd rather see it broken up.

> +	if (fd < 0) {
> +		error_errno("unable to open local file %s for pack",
> +			    preq->tmpfile.buf);
>  		goto abort;
>  	}

OK, and then we get here if we broke out of the loop due to an error
besides ENOENT/EEXIST.

> +	prev_posn = lseek(fd, 0, SEEK_END);
> +	if (prev_posn < 0) {
> +		error_errno("unable to seek local file %s for pack",
> +			    preq->tmpfile.buf);
> +		close(fd);
> +		goto abort;
> +	}
> +	preq->packfile = xfdopen(fd, "w");

And then this is the positioning magic to replace O_APPEND. Good.

> diff --git a/t/t5550-http-fetch-dumb.sh b/t/t5550-http-fetch-dumb.sh

For the record, I don't love that we are using a custom perl script here
instead of going through apache (like all of our other tests). But I
suspect the apache version would be sufficiently horrific (possibly even
worse) that it's not really worth pursuing. Hopefully this perl script
(and the accompanying fifo monstrosities) can sit here for eternity
un-looked-at by human eyes, just quietly doing their job until the heat
death of the universe.

-Peff

  reply	other threads:[~2026-07-26  9:20 UTC|newest]

Thread overview: 52+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-13 22:37 [PATCH 0/2] packfile URIs: support concurrent downloads Ted Nyman
2026-07-13 22:34 ` [PATCH 1/2] http: use unique tempfiles for packfile URI downloads Ted Nyman
2026-07-14  1:00   ` Junio C Hamano
2026-07-14  1:58     ` Ted Nyman
2026-07-14  4:07       ` Taylor Blau
2026-07-14  5:28       ` Jeff King
2026-07-14 18:10         ` Junio C Hamano
2026-07-14 18:31           ` Ted Nyman
2026-07-14  4:06   ` Taylor Blau
2026-07-14  5:44     ` Jeff King
2026-07-14  6:46   ` Jeff King
2026-07-13 22:34 ` [PATCH 2/2] fetch-pack: accept "pack" output for packfile URIs Ted Nyman
2026-07-14  7:12   ` Jeff King
2026-07-14  7:13     ` Jeff King
2026-07-14 18:38     ` Ted Nyman
2026-07-14 21:47       ` Jeff King
2026-07-14  4:13 ` [PATCH 0/2] packfile URIs: support concurrent downloads Taylor Blau
2026-07-20 22:33 ` [PATCH v2 " Ted Nyman
2026-07-20 22:33   ` [PATCH v2 1/2] http: avoid concurrent appends to partial packs Ted Nyman
2026-07-21 19:56     ` Junio C Hamano
2026-07-20 22:34   ` [PATCH v2 2/2] fetch-pack: accept "pack" output for packfile URIs Ted Nyman
2026-07-21 23:29 ` [PATCH v3 0/3] packfile URIs: support concurrent downloads Ted Nyman
2026-07-21 23:29   ` [PATCH v3 1/3] http-fetch: correct --index-pack-arg documentation Ted Nyman
2026-07-21 23:29   ` [PATCH v3 2/3] http: avoid concurrent appends to partial packs Ted Nyman
2026-07-21 23:29   ` [PATCH v3 3/3] fetch-pack: accept "pack" output for packfile URIs Ted Nyman
2026-07-24  4:43   ` [PATCH v3 0/3] packfile URIs: support concurrent downloads Junio C Hamano
2026-07-25  9:09     ` Jeff King
2026-07-25  9:21       ` Jeff King
2026-07-25 10:02         ` Jeff King
2026-07-25 10:10           ` Jeff King
2026-07-25 16:20           ` Junio C Hamano
2026-07-24  8:14   ` [PATCH v4 " Ted Nyman
2026-07-24  8:14     ` [PATCH v4 1/3] http-fetch: correct --index-pack-arg documentation Ted Nyman
2026-07-24 21:38       ` Taylor Blau
2026-07-24  8:14     ` [PATCH v4 2/3] http: avoid concurrent appends to partial packs Ted Nyman
2026-07-24  8:14     ` [PATCH v4 3/3] fetch-pack: accept "pack" output for packfile URIs Ted Nyman
2026-07-24 21:46       ` Taylor Blau
2026-07-26  6:44     ` [PATCH v5 0/3] packfile URIs: support concurrent downloads Ted Nyman
2026-07-26  6:44       ` [PATCH v5 1/3] http-fetch: correct --index-pack-arg documentation Ted Nyman
2026-07-26  6:44       ` [PATCH v5 2/3] http: avoid concurrent appends to partial packs Ted Nyman
2026-07-26  9:20         ` Jeff King [this message]
2026-07-26 10:04           ` Ted Nyman
2026-07-26 10:27             ` Jeff King
2026-07-26  6:44       ` [PATCH v5 3/3] fetch-pack: accept "pack" output for packfile URIs Ted Nyman
2026-07-26  9:21       ` [PATCH v5 0/3] packfile URIs: support concurrent downloads Jeff King
2026-07-27  0:28       ` [PATCH v6 0/6] " Ted Nyman
2026-07-27  0:28         ` [PATCH v6 1/6] http-fetch: correct --index-pack-arg documentation Ted Nyman
2026-07-27  0:28         ` [PATCH v6 2/6] http: avoid closing index-pack input twice Ted Nyman
2026-07-27  0:28         ` [PATCH v6 3/6] http: accept HTTP 416 for complete partial packs Ted Nyman
2026-07-27  0:28         ` [PATCH v6 4/6] http: avoid concurrent appends to " Ted Nyman
2026-07-27  0:28         ` [PATCH v6 5/6] http: permit unlinking partial packs on Windows Ted Nyman
2026-07-27  0:28         ` [PATCH v6 6/6] fetch-pack: accept "pack" output for packfile URIs Ted Nyman

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=20260726092027.GA3529827@coredump.intra.peff.net \
    --to=peff@peff.net \
    --cc=avarab@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=karthik.188@gmail.com \
    --cc=me@ttaylorr.com \
    --cc=ps@pks.im \
    --cc=sandals@crustytoothpaste.net \
    --cc=tnyman@openai.com \
    /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