Skip to content

perf(staged): keep gh and git child processes off the tokio runtime - #1013

Merged
matt2e merged 3 commits into
mainfrom
fix-gh-blocking-calls
Nov 2, 2026
Merged

matt2e merged 3 commits into
mainfrom
fix-gh-blocking-calls

Conversation

@matt2e

@matt2e matt2e commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Async Tauri commands and web dispatch arms were running gh, git, and other child processes directly on the shared tokio runtime. While a worker sits in a synchronous child wait, it stops driving the timer and I/O drivers. One slow gh call could freeze every other command, event, and web request. This PR moves those calls onto the blocking pool, using the same approach as the existing get_pr_for_repo / blocking_store_json code.

Calls moved off the runtime

  • GitHub commands: the 11 gh-backed commands and check_existing_local_branch now run under spawn_blocking. Their web arms use a new blocking_json helper, and blocking_store_json now delegates to it. The gh auth token and origin lookups in the async GitHub REST helpers moved too.
  • Diffs: get_diff_files, get_file_diff, get_file_at_ref (Tauri and web).
  • Utils: get_available_openers and open_in_app.
  • Branches: setup_worktree, setup_worktree_from_pr, create_remote_branch and rename_branch now share _impls that run on the pool, so the web arms call these instead of keeping their own copies. setup_worktree now also emits worktree-setup-progress and returns the branch re-read after setup. The web setup_worktree_and_run_prerun arm is detached, so a dropped request can't skip the prerun step after the worktree is created.
  • Projects and sessions: add_project_repo_impl resolves the default branch before its first store write. start_project_session builds the project context (which reads the git commit log) on the pool. The session git identity (git config --global) is read off the runtime before the claim in prepare_resume, prepare_branch_session_start and start_queued_session_for_branch. Agent discovery (login-shell command -v) runs on the pool.
  • Other places found during the sweep: web pin_repo, detect_default_branch, is_sq_available, create_project's default branch lookup, find_sq_binary in run_branch_action_impl, and update_clone_to_remote_head in detect_actions_for_repo_context.

Clone fallback

  • ensure_local_clone_with_progress now applies the captured login-shell env to its HTTPS git clone fallback, the same way ensure_local_clone does.
  • Clone error text keeps only complete stderr lines, drops \r progress updates, and is capped at the last 20 lines, so the real git error isn't buried under progress output.

Blocking guard test

web_server/blocking_guard_tests.rs scans the source of every #[tauri::command] async fn and every dispatch arm, and fails if it finds a blocking child-process call that isn't offloaded. It indexes fns by file and name, follows imports and async helpers (including git:: and blox::), and follows sync helpers up to 12 calls deep. Each ALLOWED exemption records its reason, and the test also fails on any exemption that no longer matches a call.

Out of scope

The sync create_project / create_branch main-thread paths, and rewriting spawn_gh_with_timeout on tokio::process, are left for later.

Test plan

  • Pre-push hooks passed: crates-fmt, crates-lint, crates-test, differ-ci, staged-ci
  • Guard test fails on a temporary bogus ALLOWED entry and passes once it's removed

🤖 Generated with Claude Code

matt2e and others added 3 commits November 2, 2026 13:46
Async Tauri commands and web `dispatch` arms ran gh/git/child processes
inline on the shared tokio runtime. A worker parked in a synchronous child
wait stops driving the timer and I/O drivers, so one slow `gh` call could
freeze every other command, event and web request.

Move each of those calls to the blocking pool, following the existing
`get_pr_for_repo` / `blocking_store_json` precedent:

- github_commands: the 11 gh-backed commands and `check_existing_local_branch`
  run their helper under `spawn_blocking`; the matching web arms go through
  a new `blocking_json` helper (which `blocking_store_json` now delegates to).
- diff_commands: `get_diff_files`, `get_file_diff`, `get_file_at_ref` (Tauri
  and web).
- util_commands: `get_available_openers` (mdfind + icon threads) and
  `open_in_app` move their bodies into sync helpers run on the pool.
- branches: `setup_worktree` reuses `setup_worktree_sync` under
  `spawn_blocking` via a shared `setup_worktree_impl` (same
  `BranchWithWorkdir` return and error strings; it now also emits
  `worktree-setup-progress` and returns the branch re-read after setup, so
  a de-duplicated local name is visible). `setup_worktree_from_pr` and
  `create_remote_branch` get shared `_impl`s that run their unchanged bodies
  on the pool, and the web arms call them instead of duplicating them.
  `rename_branch`'s local rename runs on the pool too.
- project_commands: `add_project_repo_impl` resolves the default branch on
  the pool before its first store write, so no await falls between writes
  (the web transport drops a request future at its next await).
- Sweep finds beyond the original list: web `pin_repo`,
  `detect_default_branch`, `is_sq_available` and `create_project`'s default
  branch lookup; the `gh auth token` / origin lookups in the async GitHub
  REST helpers; `find_sq_binary` in `run_branch_action_impl`; and
  `update_clone_to_remote_head` in `detect_actions_for_repo_context`.
  The web `setup_worktree_and_run_prerun` arm is detached so a dropped
  request cannot skip the prerun spawn after the worktree is created.

Add a source-scan guard test that walks every `#[tauri::command] async fn`
and `dispatch` arm (plus async helpers they call, one level down), strips
offloaded closures, and fails naming the offending command or arm and the
call it found.

The sync `create_project` / `create_branch` main-thread paths and a
`tokio::process` rewrite of `spawn_gh_with_timeout` are left for later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…den the guard

Address the review of 35854bf:

- Clone fallback: `ensure_local_clone_with_progress` now applies the
  captured login-shell env to its HTTPS `git clone` fallback for every
  caller (via `git::cli::configure_captured`), matching
  `ensure_local_clone`. All callers go through `setup_worktree_sync`,
  which only runs on the blocking pool.
- Clone errors: streamed stderr keeps only `\n`-terminated lines, drops
  `\r` progress updates, and caps the error text at the last 20 lines,
  so the actual git error line survives a long progress stream.
- `start_project_session` (Tauri and web) builds the project context,
  which reads the commit log through git, on the blocking pool.
- `git config --global` for the session git identity is read off the
  runtime in `prepare_resume`, `prepare_branch_session_start` and
  `start_queued_session_for_branch`, before any claim, so no await
  falls between the claim and the launch.
- Agent discovery (login-shell `command -v`) runs on the blocking pool
  for branch/review provider resolution, action providers, short-name
  generation and the web `queue_branch_session` arm.
- `add_project_repo_impl` resolves the default branch before reading
  the project's repos, so a parallel add can't race the branch name.

The source-level guard moves to `web_server/blocking_guard_tests.rs`
and now indexes every fn by file and name, follows imports, awaited
async helpers (including `git::`/`blox::`) and sync helpers up to 12
calls deep, and skips plain method calls such as store reads. Each
allow-list entry carries its reason. It catches the
`start_project_session` → `get_full_commit_log` and
`launch_resume` → `git config --global` misses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
An ALLOWED entry exempts a call for a whole fn, so one that no longer
matches would keep suppressing that call there if it came back on a
path that does block. `Crate` now records every entry a scan matches,
and the guard fails after the walk on any it never matched, naming the
file, fn and call, like the `unscanned` check does for commands.

Checked by temporarily adding a bogus `("prs.rs", "no_such_fn",
"Command::new(")` entry: the test failed with
"prs.rs: `Command::new(` in `no_such_fn`", then passed once reverted.

Also correct the `extra_env` comment in `prepare_resume`: only
`resume_session_for_store` has no await after its claim. The continue
path claims first, so the git identity read is one more await after
the claim there, like `branch_head_sha`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T05:05:48.425456Z 7e8da14 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@matt2e
matt2e merged commit b726c3e into main Nov 2, 2026
4 checks passed
@matt2e
matt2e deleted the fix-gh-blocking-calls branch November 2, 2026 05:07
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.

1 participant