Skip to content

fix: git 実行のスレッド枯渇デッドロックを根治し終了ハング・表示凍結を解消#78

Draft
sasagar wants to merge 1 commit into
devfrom
feature/fix-git-process-deadlock
Draft

fix: git 実行のスレッド枯渇デッドロックを根治し終了ハング・表示凍結を解消#78
sasagar wants to merge 1 commit into
devfrom
feature/fix-git-process-deadlock

Conversation

@sasagar

@sasagar sasagar commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

症状

  1. アプリが終了できない: 終了メニュー選択後にハングし、強制終了が必要になる(次回起動時に「アプリが適切に終了できませんでした」)
  2. org ディレクトリを開くと表示が更新されない: 履歴のリポジトリを切り替えてもコミット履歴・差分・ブランチ表示が固まったまま

根本原因

GitRunner.run / GitHubEngine.run は git/gh 1 回ごとにグローバルキューのワーカーを waitUntilExit() + group.wait() でブロックし、stdout/stderr の読み取りブロックを同じプールへ投入していた。並行実行が重なると GCD プール(ソフト上限 64)が「待つ側」で埋まり、「読む側」が永遠にスケジュールされないスターベーション・デッドロックに陥る。

  • 実機のハングレポート(LaboLabo_2026-07-07-123946.hang)で確認: 64 スレッド全部が GitRunner.swift:57group.wait() で停止、レポートに Processes reached dispatch thread soft limit (64) の明記あり
  • この状態では以後の git 呼び出しが枯渇プールに積まれるだけなので表示が一切更新されない(症状2)
  • 終了時は AppKit のウィンドウ状態保存キューが実行できず NSPersistentUIManager _waitForPendingChangesToFinish で永久待ち → 強制終了(症状1)
  • org ディレクトリでは repos × (status/numstat/履歴) × FSEvents の一斉 refresh で 64 に達しやすい

修正

  • ProcessRunner.run(async)を新設: posix_spawn + DispatchSource(read/proc) + waitpid の完全非同期実装。待機中にスレッドを 1 本も占有しない
    • NSTask の terminationHandler/readabilityHandler ベースの実装は、並行実行の負荷後に別 NSTask の終了通知が届かなくなる現象を macOS 26.5 で起こしたため不採用
    • proc source の NOTE_EXIT 登録レース(登録完了前に子が exit すると発火しない)は registration handler での再チェック + 500ms の WNOHANG フォールバックで回収
  • GitRunner / GitHubEngineProcessRunner.run へ委譲(同型バグの重複を解消)
  • ConcurrencyGate(上限 16) で git の同時プロセス数を抑制
  • AgentStatusBusaccept() 待ちを GCD キューから専用スレッドへ移し、セッション数ぶんの常駐プール消費を排除

テスト / 検証

  • swift test 全 98 テスト成功
  • 新規: 100 並列 git 呼び出しの回帰テスト(旧実装では実際にデッドロックすることを red で確認済み: 64 スレッドが dispatch_group_wait で停止するのをサンプラで観測)
  • 新規: 非同期ランナーの契約テスト(大出力 300KB×両パイプ / 空出力 / 非ゼロ exit + stderr / シグナル死 128+signo / 起動失敗 throw)、ConcurrencyGate の上限遵守テスト
  • フレーク検証: 修正前は「3 スイート併走」で 2〜7 回以内に必ずハング → proc source レース修正後は コンボ 10 連続 + 全スイート 5 連続でハングなし
  • アプリの xcodebuild ビルド成功

未検証

  • 実機アプリでの end-to-end 確認(org ディレクトリを開いて履歴切替 → 表示更新、⌘Q での正常終了)。現在起動中のインスタンスが旧バイナリ+デッドロック状態のため、再起動後の確認が必要

GitRunner/GitHubEngine は git/gh 1 回ごとにグローバルキューのワーカーを
waitUntilExit + group.wait でブロックし、stdout/stderr の読み取りブロックを
同じプールへ投入していた。並行実行が重なると GCD プール(ソフト上限 64)が
「待つ側」で埋まり、「読む側」が永遠にスケジュールされないスターベーション・
デッドロックに陥る。この状態では全 git 操作が凍結して差分/履歴/ブランチ表示が
更新されず、終了時も AppKit のウィンドウ状態保存が実行できず
_waitForPendingChangesToFinish でハングして強制終了に至っていた
(org ディレクトリを開いた際の一斉 refresh で発生しやすい)。

- ProcessRunner.run(async)を新設: posix_spawn + DispatchSource(read/proc)
  + waitpid の完全非同期実装で、待機中にスレッドを一切占有しない。
  NOTE_EXIT の登録レース(登録完了前に子が exit すると発火しない)は
  registration handler での再チェックと 500ms の WNOHANG フォールバックで回収
- GitRunner / GitHubEngine のプロセス実行を ProcessRunner.run へ委譲
- GitRunner に同時実行ゲート(ConcurrencyGate, 上限 16)を追加し、
  org ディレクトリを開いた直後の git プロセス一斉起動を抑制
- AgentStatusBus の accept() 待ちを GCD キューから専用スレッドへ移し、
  常駐分のプール消費を排除
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