Skip to content

join_pipelines: drain base_cmd stdout in a background thread#5

Merged
ryan-williams merged 2 commits into
mainfrom
fix/join-pipelines-deadlock
Jul 15, 2026
Merged

join_pipelines: drain base_cmd stdout in a background thread#5
ryan-williams merged 2 commits into
mainfrom
fix/join-pipelines-deadlock

Conversation

@ryan-williams

Copy link
Copy Markdown
Member

Summary

  • git-diff-x '<cmd>' <file> (a.k.a. gdxc <cmd> <file>) deadlocks whenever the piped output per side runs past the pipe buffer (~64 KB on Linux) — e.g. gdxc 'jq .' <big-json-file>. join_pipelines captures base_cmd's stdout via stdout=PIPE and reads it only after p.wait() returns for each pipeline. When base_cmd's output fills the pipe, it blocks writing → can't drain the FIFOs the pipelines write to → p.wait() never returns.
  • Fix: drain the captured stdout in a daemon thread so base_cmd never blocks on its own writes.

Test plan

  • New TestJoinPipelinesLargeOutput::test_large_diff_output_does_not_deadlockseq 1 30000 vs seq 30001 60000 (~340 KB diff output), guarded by SIGALRM so a broken fix fails loud instead of hanging. Passes with fix; caught deadlock on the previous behavior.
  • Manually verified gdxc 'jq .' www/public/assets/station-luc.json on a 461 KB JSON file: was hanging 2+ min, now completes in ~0.5 s with 8,653 diff lines of output.
  • Full pytest: 65 pass (was 64; +1 new test). The 3 pre-existing test_shell_integration.py failures are unrelated.

🤖 Generated with Claude Code

When `base_cmd`'s output exceeds the pipe buffer (~64 KB on Linux)
it blocks writing to its captured `stdout=PIPE`, which stops it from
draining the FIFOs the pipelines write to, which blocks the
`p.wait()` calls → deadlock. Drain the captured stdout in a daemon
thread so the writer never blocks.

Reproduces on any `git-diff-x '<cmd>' <file>` where the piped output
per side runs to hundreds of KB (e.g. `gdxc 'jq .' <big-json-file>`).

Regression test in `TestJoinPipelinesLargeOutput`: `seq 1 30000` vs
`seq 30001 60000` (~340 KB diff output); guarded by `SIGALRM` so a
broken fix fails loud instead of hanging.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a deadlock in gdxc/join_pipelines when base_cmd produces more output than the OS pipe buffer by draining base_cmd’s captured stdout concurrently, and adds a regression test to ensure large output no longer hangs.

Changes:

  • Drain base_cmd stdout in a background thread to prevent pipe-buffer backpressure deadlocking the pipelines.
  • Add a regression test that generates ~340 KB of diff output and fails fast via SIGALRM if a hang reappears.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
dffs/utils.py Drain base_cmd stdout asynchronously to prevent pipe-buffer deadlocks during join_pipelines.
tests/test_join_pipelines.py Add a large-output regression test guarded by an alarm to catch hangs reliably.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…test

Follow-ups on the deadlock fix:

- `dffs/utils.py`: the stdout-drain thread previously swallowed any
  exception from `proc.stdout.read()`, silently returning empty output
  with a success code. Capture the exception and re-raise it in the main
  thread after `join()` instead.
- `tests/test_join_pipelines.py`: assert the complete diff output (hunk
  header, all 30000 `<` lines, `---`, all 30000 `>` lines), verifying the
  drain captured every byte rather than only that it didn't deadlock.
  Capture via `monkeypatch` on `dffs.utils.stdout` since the module's
  import-time `from sys import stdout` binding defeats `capsys`/`capfd`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ryan-williams
ryan-williams merged commit b37b140 into main Jul 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants