huskies: merge 1151 bug install_pre_commit_hook blocks the tokio executor — sync std::process::Command::output() in an async path stalls worktree-create-sub

This commit is contained in:
dave
2026-05-19 18:14:51 +00:00
parent 1db5473f50
commit 918f18c200
2 changed files with 55 additions and 1 deletions
+7 -1
View File
@@ -129,7 +129,13 @@ pub(crate) async fn on_coding_transition(project_root: &Path, port: u16, story_i
"[worktree-create-sub] Worktree ready for '{story_id}' at {}",
info.path.display()
);
if let Err(e) = crate::worktree::install_pre_commit_hook(&info.path) {
let hook_path = info.path.clone();
let hook_result = tokio::task::spawn_blocking(move || {
crate::worktree::install_pre_commit_hook(&hook_path)
})
.await
.unwrap_or_else(|e| Err(format!("spawn_blocking panicked: {e}")));
if let Err(e) = hook_result {
slog_warn!(
"[worktree-create-sub] Pre-commit hook install failed for '{story_id}': {e}"
);
+48
View File
@@ -475,6 +475,54 @@ mod tests {
);
}
/// Regression: before the fix, `install_pre_commit_hook` was called directly
/// from an async context, monopolising a tokio worker thread per call. With
/// `worker_threads=2` and two concurrent installs the executor would stall.
/// After the fix the installs run on `spawn_blocking`'s thread pool, leaving
/// both worker threads free to drive async tasks like the sleep below.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn install_pre_commit_hook_does_not_block_tokio_executor() {
let tmp = TempDir::new().unwrap();
// Helper: create a git repo and a linked worktree inside tmp.
let setup = |repo: &str, wt: &str, branch: &str| -> std::path::PathBuf {
let repo_path = tmp.path().join(repo);
fs::create_dir_all(&repo_path).unwrap();
init_git_repo(&repo_path);
Command::new("git")
.args(["config", "extensions.worktreeConfig", "true"])
.current_dir(&repo_path)
.output()
.unwrap();
let wt_path = tmp.path().join(wt);
Command::new("git")
.args(["worktree", "add", wt_path.to_str().unwrap(), "-b", branch])
.current_dir(&repo_path)
.output()
.unwrap();
wt_path
};
let wt1 = setup("repo1", "wt1", "feature/noblock-1");
let wt2 = setup("repo2", "wt2", "feature/noblock-2");
// Both hook installs run on the blocking pool — executor threads stay free.
let h1 = tokio::task::spawn_blocking(move || install_pre_commit_hook(&wt1));
let h2 = tokio::task::spawn_blocking(move || install_pre_commit_hook(&wt2));
let start = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let elapsed = start.elapsed();
h1.await.unwrap().expect("hook install 1 must succeed");
h2.await.unwrap().expect("hook install 2 must succeed");
assert!(
elapsed < std::time::Duration::from_millis(500),
"tokio executor was starved; 100 ms sleep took {elapsed:?}"
);
}
#[test]
fn install_pre_commit_hook_creates_executable_hook_and_sets_hookspath() {
let tmp = TempDir::new().unwrap();