Skip to content

fix(node,git): bound a hung served git with a total-duration timeout (#62)#165

Open
beardthelion wants to merge 4 commits into
mainfrom
fix/served-git-timeout
Open

fix(node,git): bound a hung served git with a total-duration timeout (#62)#165
beardthelion wants to merge 4 commits into
mainfrom
fix/served-git-timeout

Conversation

@beardthelion

@beardthelion beardthelion commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Part 2 of #62 (part 1, the teardown-wiring test, merged in #150).

A served git upload-pack/receive-pack that neither finishes nor disconnects parks forever, pinning a git PID and, for receive-pack, holding the repo write lock. upload-pack is reachable by an anonymous caller on a public repo, and there is no tower timeout layer (axum imposes no default), so an unauthenticated request can wedge a worker indefinitely.

This wraps the child interaction in tokio::time::timeout, bounded by a new GITLAWB_GIT_SERVICE_TIMEOUT_SECS (default 600s, must be positive). The stdin write and the stdout/stderr drain run concurrently under one deadline so a large body cannot deadlock. On expiry the whole process group is torn down (SIGTERM, then SIGKILL past a ~2s grace, hard-capped) and reaped before returning, so a caller releasing the receive-pack write lock cannot race a still-live git. The handlers map the typed timeout to a distinct 504 via a pure classifier, clear of the 500 git error and the 404/401 a client keys retries on, rather than string-matching the anyhow chain.

Deliberately out of scope, documented on the config knob: the info/refs advertisement and the withheld-blob path (upload_pack_excluding, a spawn_blocking a tokio timeout cannot cancel) stay unbounded. A follow-up to bound those will come separately.

Tests: the timeout fires and tears the group down for both upload-pack and receive-pack; the whole group including grandchildren is reaped before returning, with the SIGKILL escalation load-bearing; the 504 mapping, every classifier arm, and the config rejecting 0 are covered. Full gitlawb-node suite green.

Summary by CodeRabbit

  • New Features
    • Added a configurable wall-clock timeout for served git operations (default 600s), with CLI/env support and positive-value validation.
    • Timeout failures now return a distinct gateway-timeout response (504) for git requests.
  • Bug Fixes
    • Improved smart HTTP error classification, returning a client error for malformed requests (e.g., bad line length/protocol issues) instead of a generic server error.
    • Improved timeout handling to terminate and clean up the entire running git operation reliably, and preserved response ordering so git stderr is reported before related input failures.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3be5419f-9cd2-42b7-bd55-33d9fe7da201

📥 Commits

Reviewing files that changed from the base of the PR and between 41eca0b and 957116c.

📒 Files selected for processing (3)
  • .env.example
  • README.md
  • crates/gitlawb-node/src/git/smart_http.rs
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/gitlawb-node/src/git/smart_http.rs

📝 Walkthrough

Walkthrough

Adds a configurable timeout for served git smart-HTTP operations, maps timeout failures to HTTP 504, enforces the deadline in run_git_service, and updates request handlers to classify and log timeout, bad-request, and generic git errors.

Changes

Git service timeout enforcement

Layer / File(s) Summary
Config and timeout response mapping
crates/gitlawb-node/src/config.rs, crates/gitlawb-node/src/error.rs, .env.example, README.md
Adds git_service_timeout_secs, its CLI/env configuration, and AppError::Timeout with HTTP 504 mapping and tests.
Bound git subprocess execution
crates/gitlawb-node/src/git/smart_http.rs
upload_pack and receive_pack accept a timeout, and run_git_service now enforces it while draining child I/O and reaping process groups on timeout.
Timeout and stderr behavior tests
crates/gitlawb-node/src/git/smart_http.rs
Adds and updates tests for hung requests, process-group cleanup, receive-pack parity, and stderr precedence over stdin EPIPE.
Classify git service errors in handlers
crates/gitlawb-node/src/api/repos.rs
Adds git_service_app_error, wires the configured timeout into both handlers, and updates logging and tests for timeout/bad-request/git classifications.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • Gitlawb/node#28: Overlaps the git_upload_pack integration point in crates/gitlawb-node/src/api/repos.rs.
  • Gitlawb/node#61: Touches the same run_git_service and process-group teardown path in crates/gitlawb-node/src/git/smart_http.rs.
  • Gitlawb/node#150: Also changes run_git_service behavior and related teardown-focused tests in crates/gitlawb-node/src/git/smart_http.rs.

Suggested labels: sev:medium, kind:test

Suggested reviewers: jatmn, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change and motivation, but it does not follow the required template sections or include a verification command block. Add the template headings (Summary, Motivation & context, Kind of change, What changed, How a reviewer can verify) and fill in a command example.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a total-duration timeout for served git operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/served-git-timeout

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gitlawb-node/src/api/repos.rs`:
- Around line 927-941: The push-path classifier in git_receive_pack is missing
the AppError::BadRequest arm that git_upload_pack already handles, so malformed
pushes are being logged at error level instead of warn. Update the match inside
repos.rs’s receive-pack error handling to mirror the fetch-path classification
in git_upload_pack: keep Timeout as warn, add a BadRequest warn branch, and
leave only unexpected failures in the error branch while still returning the
mapped AppError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8c06bcf-7ffd-4301-a85c-97b37da698e0

📥 Commits

Reviewing files that changed from the base of the PR and between 67ad2b8 and a18d631.

📒 Files selected for processing (4)
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/error.rs
  • crates/gitlawb-node/src/git/smart_http.rs

Comment thread crates/gitlawb-node/src/api/repos.rs
@beardthelion beardthelion added crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior labels Jul 7, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one issue that needs to be addressed before this is ready.

Findings

  • [P3] Complete CodeRabbit's request to mirror BadRequest logging on receive-pack
    crates/gitlawb-node/src/api/repos.rs:935
    CodeRabbit's inline request is still valid: git_service_app_error can classify malformed push input as AppError::BadRequest, but the git_receive_pack logging match only handles Timeout separately and sends every other mapped error through the generic tracing::error! branch. That means client-caused malformed pushes return the right 400 but are still logged as server errors, unlike the upload-pack path. Please complete that review item by adding the AppError::BadRequest warn branch here and leaving only unexpected failures in the error branch.

t added 3 commits July 7, 2026 20:00
…#62)

Part 1 of #62. A served git upload-pack/receive-pack that neither finishes
nor disconnects parks forever, pinning a git PID (and, for receive-pack,
holding the write lock); on a public repo an anonymous caller can trigger
it. No tower TimeoutLayer exists and axum imposes no default.

Wrap the child interaction in tokio::time::timeout, bounded by a new
GITLAWB_GIT_SERVICE_TIMEOUT_SECS knob (default 600, must be positive).
Drain stdout/stderr concurrently with the stdin write so a large body
can't deadlock. On expiry run_git_service returns a typed GitServiceTimeout
and reaps the WHOLE process group before returning (SIGTERM so git clears
its .git/*.lock, wait for the leader and its grandchildren like index-pack
to exit, escalate to SIGKILL past a grace, hard-capped), so a caller
releasing the receive-pack write lock can't race a still-live git on the
same repo.

The handlers map GitServiceTimeout to a distinct 504 (not the generic 500
git error, and clear of the read-gate 404 / auth 401 the client keys
retries on) via a pure, unit-testable classifier rather than matching the
anyhow string. Surface git's own non-zero exit (its stderr) before a
stdin-write EPIPE so a malformed body is classified 400, not 500.

Out of scope, deferred in #62: the info/refs advertisement and the
withheld-blob path (spawn_blocking, which tokio timeout cannot cancel);
both remain unbounded (noted in the config knob's docs).

Tests: the timeout fires and tears the group down; it reaps the whole
group before returning (RED if the reap is leader-only); the wrap is
load-bearing (RED via an outer bound, not a hang); the error must be the
typed timeout; the 504 mapping and classifier are unit-tested; config
rejects 0. Full suite green.
- test(node): cover the `protocol error` arm of git_service_app_error
  independently (the input has no "bad line length" substring, so it
  isolates the second classifier arm and goes RED if that arm is removed)
- test(node): make the reap-before-return test's grandchild IGNORE SIGTERM
  (`trap "" TERM`) and outlive the ~4s reap cap, so the SIGKILL escalation
  is load-bearing. Previously the grandchild self-exited under the cap, so
  neutering the SIGKILL still passed; the escalation was exercised but not
  required. Verified: RED with SIGKILL disabled, GREEN with it.
- test(node): add a receive-pack timeout test. Every prior timeout test used
  an upload-pack fake; this proves the push path (which also holds the repo
  write lock) is bounded too. RED-verified with the internal timeout removed.
- test(node): poll for the pidfile in the reap-before-return timeout test
  so a loaded runner can't false-panic before the fake git writes it
- remove the redundant `sigkilled` guard in reap_group_on_timeout; step
  == 200 fires exactly once in the 0..400 loop, so no re-entry guard is
  needed and the SIGKILL still fires exactly once
git_receive_pack's error classifier routed every non-Timeout error,
including client-caused BadRequest (400), through tracing::error!. Mirror
the git_upload_pack arm so a malformed push logs at warn level instead of
as a server error.
@beardthelion beardthelion force-pushed the fix/served-git-timeout branch from a18d631 to 41eca0b Compare July 8, 2026 01:06
@beardthelion

Copy link
Copy Markdown
Collaborator Author

@jatmn added the AppError::BadRequest warn arm on the receive-pack path, mirroring the upload-pack classifier as you and CodeRabbit flagged. Pushed as 41eca0b (also rebased onto current main). Ready for another look.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a couple of issues that need to be addressed before this is ready.

Findings

  • [P2] Make the smart_http tests pass under the normal parallel test runner
    crates/gitlawb-node/src/git/smart_http.rs:1097
    The new timeout tests pass when run by themselves, but the normal grouped module run still fails under the default parallel runner: cargo test -p gitlawb-node git::smart_http::tests -- --nocapture panics with ``async fn resumed after completion from this polling loop. The loop ignores the `Ok(result)` case from `tokio::time::timeout(..., &mut fut)`, so if `run_git_service` returns before the pidfile is observed under parallel process-test load, the next iteration polls an already-completed future and aborts the test. Please make this test handle early completion explicitly, or otherwise isolate the process tests, so the changed timeout coverage is stable under the same parallel mode as `cargo test --workspace`.

  • [P3] Add the new timeout knob to the env reference docs
    .env.example:90
    The PR adds GITLAWB_GIT_SERVICE_TIMEOUT_SECS in Config with a default of 600 seconds, but the operator-facing env reference still jumps from GITLAWB_PUBLIC_READ to GITLAWB_MAX_PACK_BYTES, and the README says .env.example is the full configuration reference. Operators relying on that documented reference will not discover the new bound or the documented caveat that info/refs and upload_pack_excluding remain unbounded. Please add the new variable to .env.example and, if it is considered an important node setting, the README configuration table as well.

… runner (#165)

Addresses jatmn's review of 41eca0b.

[P2] The #62 timeout tests failed under `cargo test --workspace`:
- run_git_service_tears_down_group_when_future_dropped re-polled a completed
  future ("async fn resumed after completion"): the advance-until-pids loop
  ignored the timeout's Ok(_) (future-finished) case and polled the resolved
  future again. It now breaks on early completion.
- Under fork-storm load a freshly-written fake `git` transiently fails to exec
  (ETXTBSY) or is timed out before recording its pids. Added a
  fake_git_run_with_pids retry helper (used by the four pid-reading tests) plus
  a matching inline retry on the drop test, with growing backoff for the
  correlated bursts and a 12-attempt cap that still fails loudly on a genuine
  never-spawns regression. Reverted two tests from a speculative 2000ms bound
  back to 300ms/1000ms now that the retry covers the timeout-vs-pidfile race.

[P3] Documented GITLAWB_GIT_SERVICE_TIMEOUT_SECS in .env.example and the README
config table (default 600; bounds upload-pack/receive-pack, not info/refs or the
withheld-blob path).

The tests stay load-bearing: breaking process_group(0), the post-reap disarm,
the timeout-arm reap, or the internal timeout each still turns the corresponding
test red (mutation-verified).
@beardthelion

Copy link
Copy Markdown
Collaborator Author

@jatmn both addressed, pushed as 957116c.

[P2] The re-poll: run_git_service_tears_down_group_when_future_dropped ignored the Ok(_) (future-finished) result from timeout(&mut fut) and re-polled a completed future; it now breaks on early completion. Verifying that under the parallel runner surfaced a second, load-dependent flake: a freshly-written fake git transiently fails to exec (ETXTBSY) or gets timed out before writing its pidfile. Added fake_git_run_with_pids (retry with backoff) to the pid-reading tests plus a matching inline retry on the drop test, and reverted a stopgap 2000ms bound to the original 300ms/1000ms now that the retry covers the timeout-vs-pidfile race. Clean over 360 concurrent module runs, and the invariants stay load-bearing: removing process_group(0), the disarm, the reap, or the internal timeout each still turns its test red.

[P3] GITLAWB_GIT_SERVICE_TIMEOUT_SECS added to .env.example and the README config table (default 600; bounds upload-pack/receive-pack, not info/refs or the withheld-blob path).

@beardthelion beardthelion requested a review from jatmn July 8, 2026 05:16

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants