gvfs-helper: parallelize POST requests - #980
gvfs-helper: parallelize POST requests#980Derrick Stolee (derrickstolee) wants to merge 3 commits into
Conversation
| installation of multiple prefetch packs. Values less than `1` are | ||
| treated as `1`. | ||
|
|
||
| gvfs.postThreads:: |
There was a problem hiding this comment.
Does this feature ever tend to bottleneck on CPU (ie when indexing pack files) or only on network/remote?
I'm wondering if there'd be any benefit or downside to supporting "values less than 1 are treated as NUMBER_OF_PROCESSORS" like checkout.workers does.
There was a problem hiding this comment.
Also, GVFS uses Environment.ProcessorCount as the default parallelism value for its analogous workflow (gvfs prefetch --files or --folders)
There was a problem hiding this comment.
We could consider the "values less than one" option. I worry that the network will saturate at a lower parallelism than the CPU doing pack-indexing.
Large object requests currently fetch batches sequentially, leaving network capacity unused when server latency dominates the transfer. Allow callers to select multiple POST workers through a new gvfs.postThreads configuration value. Default the value to one so existing users retain the sequential path, and treat values below one as one. Document how higher values enable concurrent HTTP requests that stream into separate index-pack processes. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fetching a large set of missing objects through gvfs-helper performs each HTTP POST and index-pack operation sequentially. This leaves the client waiting on individual network transfers even when the server and local machine can support concurrent work. Use a mutex-protected queue to distribute full object batches across worker threads. Each worker owns an HTTP-subsystem-configured curl handle and streams each response into a fresh index-pack process, including cache-server fallback attempts. Initialize curl handles before starting threads to retain configured proxy, TLS, user-agent, and timeout behavior. Serialize child startup while marking pipe descriptors close-on-exec so concurrent index-pack children cannot keep sibling pipes open. Keep OID formatting and result collection thread-local, propagate transfer and pack installation failures, and avoid touching temporary packs owned by other processes. Partition work into batches containing at least two objects because a single non-commit object can be returned loose instead of as a pack. Continue using the existing sequential path when threading is unavailable or the configured block size cannot satisfy that constraint. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise gvfs-helper POST requests with both one and four configured workers so the sequential and parallel paths must fetch identical object sets. Cover multiple batches, a final single-OID remainder, and duplicate requests while checking both installed objects and packfile counts. Register the new script in the Meson integration test list so Meson builds run the same coverage as the default test harness. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Neil Kainga <t-neilkainga@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f05b1d5 to
6b263d8
Compare
tyrielv
left a comment
There was a problem hiding this comment.
Automated multi-perspective review (Review Swarm): six independent reviewer lenses run on diverse models, findings deduplicated and human-reviewed before posting. Four comments follow. The concurrency scaffolding itself held up well under review — fd lifecycles, the CLOEXEC/spawn_mutex deadlock fix, per-thread curl handle cleanup, and the block partitioner all look sound, and no TLS or credential weakening was found. The comments concentrate on behavioral parity between the new parallel path and the existing sequential one.
| continue; | ||
| } | ||
|
|
||
| if (res != CURLE_OK) |
There was a problem hiding this comment.
Note
🤖 Machine-drafted review comment (Review Swarm) — reviewed & approved by tyrielv before posting
[correctness + risk-rollout + security — convergent across 3 independent model families] 🟠 HIGH
The threaded POST path calls curl_easy_perform directly and collapses every non-200/non-404 response into a fatal GH__ERROR_CODE__INDEX_PACK_FAILED, then breaks. The sequential path (gh__response_status / GH__RETRY_MODE__*, ~898-936 and ~3496-3573) handles 401 by reloading main credentials and re-authenticating, and 429/503 as transient retries with backoff and Retry-After.
The parallel worker does none of this. An expired auth token or server throttling — both routine against real GVFS/Azure endpoints, which is exactly the dogfooding target — aborts the whole post where today's code recovers. Two aggravating details:
- HTTP/curl failures are surfaced as
index-pack failed, which will mislead anyone triaging from telemetry. - Because workers never call
credential_reject/credential_approve, each worker keeps sending the same rejected secret concurrently and the bad credential is never invalidated in the helper.
This changes the failure semantics of gvfs-helper post, not just its speed, for anyone who sets gvfs.postThreads > 1.
| * Start a fresh index-pack before falling back to the main | ||
| * server so the two response bodies cannot be concatenated. | ||
| */ | ||
| if (can_fallback && |
There was a problem hiding this comment.
Note
🤖 Machine-drafted review comment (Review Swarm) — reviewed & approved by tyrielv before posting
[risk-rollout] 🟠 HIGH
Two fallback-semantics divergences from the sequential path:
-
Cache-server 404 no longer falls back to origin. It is excluded from fallback and instead retried (up to six times, with delays that synchronize across workers) against the same cache server. The sequential path falls back to origin on a cache 404. Cold-cache objects therefore become partial failures precisely under the workload parallel mode was built for, and the synchronized retries multiply load on an already-missing cache.
-
gvfs.fallback=false/--no-fallbackappears to be ignored. At theif (gh__global.cache_server_url)block indo__http_post__fetch_oidset(~4416), an origin fallback URL is built whenever a cache URL exists, without consultinggh__cmd_opts.try_fallback. That would defeat an explicit origin-load containment control operators rely on during an incident.
| * their final locations. Tolerates races where another thread or | ||
| * process installed the same packfile concurrently. | ||
| */ | ||
| static int my_finalize_packfile_simple(const char *temp_pack, |
There was a problem hiding this comment.
Note
🤖 Machine-drafted review comment (Review Swarm) — reviewed & approved by tyrielv before posting
[design] 🟡 MEDIUM
The doc comment says this tolerates races where another thread or process installed the same packfile concurrently, but I don't think it does.
my_finalize_packfile has an explicit assume_ok path: when finalize_object_file_flags fails, it checks file_exists(final_path_pack) && file_exists(final_path_idx) and treats that as success. my_finalize_packfile_simple returns -1 instead, and the caller (~4309) turns that into GH__ERROR_CODE__INDEX_PACK_FAILED and breaks out of the worker loop — failing the whole post.
That race seems most likely on exactly this path: two workers producing the same pack hash, or two gvfs-helper processes sharing an ODB, which the PR description calls out. The narrower signature looks deliberate and right — dropping status in particular makes sense since it isn't thread-safe. It's just this one property that the comment promises and the body doesn't implement.
| test_expect_success "post blobs ($mode, threads=$threads)" ' | ||
| test_when_finished "per_test_cleanup" && | ||
| start_gvfs_protocol_server && | ||
| git -C "$REPO_T1" config gvfs.postThreads '$threads' && |
There was a problem hiding this comment.
Note
🤖 Machine-drafted review comment (Review Swarm) — reviewed & approved by tyrielv before posting
[tests] 🟠 HIGH
Three gaps, the first of which makes the new coverage largely nominal:
-
The parallel cases never verify the parallel path ran. They set
gvfs.postThreads=4and exportGIT_TRACE2_EVENT, but never read the trace back. The siblingt5797-gvfs-helper-prefetch-threads.shassertstest_trace2_data gvfs-helper prefetch/install_mode $expected_modeafter every parallel test. Without that, ifHAVE_THREADSis 0 or the entry gate rejects the batch (nr_oids <= 1,block_size <= 1), the sequential fallback runs and all four tests still pass. Relatedly there is noHAVE_THREADSprerequisite, so a threadless build reportsparallel tests passwith no parallel code executed. -
No regression test for the pipe-inheritance deadlock — the bug that motivated the series.
start_command_cloexecunderspawn_mutexis subtle enough that a future refactor could revert it silently. Such a test needs threads >= 2, enough batches for >= 2 concurrentindex-packspawns, and a hard timeout — otherwise a regression hangs CI instead of failing. -
No error-path coverage.
t5797hasdo_prefetch_corrupt_packviastart_gvfs_protocol_server_with_mayhem;t5798has no analogue. Nothing exercises a worker'sindex-packfailing, a non-200, or a mid-stream close — sopost_worker_thread_fn's error aggregation (td->ec,td->error_message) is entirely untested.
Improve full-clone performance by allowing
gvfs-helper postto download object batches concurrently. The newgvfs.postThreadsconfiguration defaults to 1, preserving the existing sequential behavior unless explicitly enabled.The parallel implementation uses independent curl handles and
index-pack --stdinchildren with a mutex-protected work queue. It preserves Git HTTP configuration, supports threadless builds through the sequential fallback, and avoids sending singleton loose-object responses toindex-pack.Dogfood testing exposed a deadlock caused by sibling
index-packprocesses inheriting one another's pipe descriptors. The series serializes child creation and marks parent pipe descriptors close-on-exec. It also removes the sharedoid_to_hex()buffer race, protects SIGPIPE handling, isolates retry child lifecycles, closes error-path descriptors, and avoids deleting temporary packs belonging to concurrent processes using the shared ODB.Neil Kainga diagnosed and tested the pipe inheritance fix on a 1JS full clone with
gvfs.postThreads=8and is credited in the fix commits.The focused
t5798-gvfs-helper-post-threads.shcoverage exercises sequential and parallel requests, small batches, singleton remainders, duplicate downloads, and object integrity.