Skip to content

fix(cli-push-runner): diff stage の timeout 欠落を修正 (push パイプライン改善 T6) - #283

Merged
aloekun merged 1 commit into
masterfrom
fix/diff-stage-timeout
Jul 17, 2026
Merged

fix(cli-push-runner): diff stage の timeout 欠落を修正 (push パイプライン改善 T6)#283
aloekun merged 1 commit into
masterfrom
fix/diff-stage-timeout

Conversation

@aloekun

@aloekun aloekun commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • diff stage の Command::output() (無限待ち) を timeout 付き実行に載せ替え、超過時は DiffResult::Error = exit 5 で中断する (fail-closed)
  • [diff] timeout を追加 (未指定は既定 60s)。[push] timeout と同形の escape hatch
  • stdout / stderr の分離を維持。run_cmd_shell_* は 3 variant すべてが両者を結合するため使わず、callsite 実装に留めた
  • 回帰テスト 9 本を追加 (mod t6_diff_timeout 7 + config 2)。timeout の検証は 経過時間を assert する
  • ADR-044 に「variant を追加しなかった」判定を記録 (T5 と逆の結論になったため)
  • 実装中に見つけた lib-subprocess の同型欠陥は本 PR では触れず backlog に登録

Context

Why: stages/diff.rsrun_diff_cmdCommand::output() で子プロセスの終了を無限に待っていた。他 stage は全て timeout 付き (jj 系 30s / gate 300s / push 300s) で、diff だけが穴だった。ADR-045 の並列 workspace 運用で jj の lock 競合が起きると、pnpm push は診断も timeout も無いまま停止し手動 kill するしかない。

Trigger: docs/push-pipeline-fix-plan.md の T6 (2026-07-16 の push パイプライン調査)。in the wild の発火記録は無く、T5 と同じくコード監査で「他 stage は全て timeout 付き」という非対称として特定されたもの。

なぜ T5 の run_cmd_shell_unlimited を使わないか: run_cmd_shell_* は 3 variant すべてが combine_output で stdout と stderr を結合する。diff の stdout は reviewers が読むレビュー対象そのものとしてファイルに書かれるため、jj が stderr に出す警告 (並列 workspace 運用時の Concurrent modification detected = まさに本 PR が想定する状況) の混入は許容できない。結合の有無は variant の軸 (drain 戦略) では表現できず、4 つ目の variant は骨格に載らない別 family の新設になるため、ADR-044 層 1 に従い callsite に残置した。

実装上の落とし穴 (回帰テストが初版を検出): 「timeout 後に reader thread を join する」初版は timeout 1s に対し制御が戻るまで 9.6s 掛かった。cmd /c <command> の child は cmd.exe で、孫 (実際の jj) は child.kill() の対象外。孫が pipe の書き込み端を保持したままなので EOF が来ず、join が孫の自然終了までブロックする = timeout が意味を成さない。child を kill した 2 経路では join せず detach する形に修正した。

Scope decision: 実装中に lib-subprocessrun_cmd_shell_* 3 variant が同じ穴を持つことが判明した (実測 9.23s / timeout_secs = 1 指定)。影響は quality_gate step_timeout / push timeout / cli-merge-pipeline。計画 §2 原則 4 (1 PR 1 変更) に従い本 PR では触れず、§6 backlog 10 に登録した。

Validation

  • cargo test -p cli-push-runner: 215 pass (206 → 215、+9 本)
  • cargo test --workspace: 1557 pass / --ignored スイートも pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings: warning 0
  • pnpm push pre-push review (pre-push-review-refute, 2026-07-17): verdict=APPROVE (simplicity / security とも、fix iteration 0)。dogfood 実測 338s (pre_checks 1.2s / quality_gate 48.9s / diff 0.1s / takt 285.1s / push 2.3s)
  • 回帰テストが素通りしないことの実証: cli-push-runner のテスト全体が 9.66s → 1.55s に短縮 (= timeout が実際に効いている証跡)
  • サンドボックス実機 before/after: [diff] commandping -t (永久応答 = 返らない jj diff の代役) にし、@- から build した修正前 exe と比較。before は diff stage の所要時間が外側 kill にそのまま追随 (25s→24.4s / 10s→9.4s) = 内部に上限が無く、放置すれば無限待ち・診断なし。after は 3.0s で exit 5 + 「jj lock 競合を疑え」の診断。実 jj diff (既定 60s) が誤 timeout せず 510 行を書き出すことも確認
  • 副次的実証: before の run 後に ping.exe が残存し、孫が kill を生き延びる (= join がブロックする) ことを実機で裏付け

References

Summary by CodeRabbit

  • 新機能
    • 差分生成ステップにタイムアウト(既定60秒)を追加しました。
    • [diff] timeout でタイムアウト時間を変更できます。
  • バグ修正
    • 差分生成がハングした場合、一定時間で安全に失敗終了するよう改善しました。
    • タイムアウト時は差分ファイルを作成せず、終了コード5で処理を中断します。
    • 標準エラー出力が差分内容に混入しないよう修正しました。
  • ドキュメント
    • 設定ファイルと運用ガイドに、タイムアウトの挙動と設定方法を追記しました。

stages/diff.rs の run_diff_cmd は Command::output() で子プロセスの終了を無限に
待っていた。他 stage は全て timeout 付き (jj 系 30s / gate 600s / push 300s) で、
diff だけが穴だった。ADR-045 の並列 workspace 運用で jj の lock 競合が起きると、
pnpm push は診断も timeout も無いまま停止し、手動 kill するしかない。

変更:
- run_diff_cmd を spawn + drain_pipe_unlimited × 2 + wait_with_timeout_safe に
  載せ替え。timeout 時は Err → DiffResult::Error = exit 5 で中断する
  (fail-closed / ADR-043)。診断に超過秒数・コマンド・jj lock 競合を疑う旨を含める。
- DiffConfig に timeout: Option<u64> を追加 (未指定は 60s = DEFAULT_DIFF_TIMEOUT_SECS)。
  [push] timeout と同形。60s は jj 系 30s より長め: diff は working copy の
  snapshot + 大 diff の書き出しを伴い、jj bookmark list (読み取りのみ) より重い。
  timeout の目的はハング検知でありlatency 制限ではないため、誤 timeout で
  pipeline 全体を落とすより余裕側に倒す (値はユーザー承認済み)。

T5 の run_cmd_shell_unlimited を使わない理由: run_cmd_shell_* は 3 variant すべてが
combine_output で stdout と stderr を結合する。diff の stdout は reviewers が読む
レビュー対象そのものとしてファイルに書かれるため、jj が stderr に出す警告 (並列
workspace 運用時の Concurrent modification detected = まさに本 PR が想定する状況) の
混入は許容できない。同型の「全量 + 分離 + timeout」は bookmark_check の
run_jj_bookmark_list にもあるが、direct args で signature 非互換のため共通化しない
(ADR-044 層 1)。variant を追加しなかった判定は ADR-044 に記録した。

実装上の落とし穴 (回帰テストが初版を検出):
timeout 後に reader thread を join する初版は、timeout 1s に対し制御が戻るまで
9.6s 掛かった。cmd /c <command> の child は cmd.exe で、孫 (実際の jj) は
child.kill() の対象外。孫が pipe の書き込み端を保持したままなので EOF が来ず、
join が孫の自然終了までブロックする = timeout が意味を成さない (本 PR が直す
ハングの再生産)。child を kill した 2 経路では join せず detach して即座に返す。
教訓: timeout の回帰テストは Err の内容だけでなく経過時間を assert すること。

検証:
- 回帰テスト mod t6_diff_timeout 7 本 + config 2 本 (ADR-049 の流儀。206 → 215 pass)。
  bad = 応答しないコマンドを timeout で打ち切り 5s 以内に制御を返すこと /
  good = timeout 内に終わるコマンドを誤って打ち切らないこと。「stderr を diff に
  混ぜない」契約も seal (run_cmd_shell_* に載せ替えると落ちる)。
  cli-push-runner のテスト全体が 9.66s → 1.55s = timeout が効いている証跡。
- サンドボックスの jj repo で [diff] command を ping -t (永久応答 = 返らない
  jj diff の代役) にし、@- から build した修正前 exe と比較。before は diff stage の
  所要時間が外側 kill に追随 (25s→24.4s / 10s→9.4s) = 内部に上限が無く放置すれば
  無限待ち・診断なし。after は 3.0s で exit 5 + 診断あり。実 jj diff (既定 60s) が
  誤 timeout しないことも確認。before の run 後に ping.exe が残存し、孫が kill を
  生き延びる実機裏付けも取れた。
- cargo clippy --workspace --all-targets --all-features -- -D warnings で warning 0、
  cargo test --workspace 1557 pass、--ignored スイートも pass。

発見 (本 PR 外): lib-subprocess の run_cmd_shell_* 3 variant が同じ穴を持ち、
timeout が wall-clock を縛れない (実測 9.23s / timeout_secs = 1 指定)。影響は
quality_gate step_timeout / push timeout / cli-merge-pipeline。1 PR 1 変更のため
本 PR では触れず、計画書 §6 backlog 10 に登録した。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b97fcd50-c420-4a85-b286-af78fe286bcd

📥 Commits

Reviewing files that changed from the base of the PR and between a519232 and c36cec5.

📒 Files selected for processing (6)
  • docs/adr/adr-044-subprocess-utility-extraction-boundary.md
  • docs/push-pipeline-fix-plan.md
  • push-runner-config.toml
  • src/cli-push-runner/src/config/mod.rs
  • src/cli-push-runner/src/stages/diff.rs
  • templates/push-runner-config.toml

📝 Walkthrough

Walkthrough

diff ステージに既定60秒の timeout、stdout/stderr分離、timeout時の fail-closed 処理を追加し、設定パース・回帰テスト・運用ドキュメント・ADRを更新した。

Changes

diff timeout 設定と実行

Layer / File(s) Summary
timeout 設定契約
src/cli-push-runner/src/config/mod.rs, push-runner-config.toml, templates/push-runner-config.toml
DiffConfig.timeout と既定値60秒を追加し、未指定時の既定値および明示値のパースをテスト・文書化した。
diff コマンドの timeout 実行
src/cli-push-runner/src/stages/diff.rs
stdout/stderrを分離して排出し、wait_with_timeout_safe で監視する実装へ変更した。timeout時は DiffResult::Error とし、diffファイルを作成しない回帰テストを追加した。
境界判断と完了記録
docs/adr/adr-044-subprocess-utility-extraction-boundary.md, docs/push-pipeline-fix-plan.md
新しい subprocess variant を追加しない判断、reader thread の課題と backlog、T6の実装結果および実機検証を記録した。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiffStage
  participant ChildProcess
  participant PipeDrainers
  participant WaitWithTimeoutSafe
  DiffStage->>ChildProcess: spawn diff command
  ChildProcess->>PipeDrainers: separate stdout and stderr
  DiffStage->>WaitWithTimeoutSafe: wait with configured timeout
  WaitWithTimeoutSafe-->>DiffStage: result or timeout
  DiffStage-->>DiffStage: return stdout or DiffResult::Error
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed diff stage の timeout 欠落修正という主変更を正確に表しており、PR の内容と整合しています。
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/diff-stage-timeout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Monitor 分析 (GitHub Actions バックストップ)

  • トリガー: issue_comment (created) / 実行 run
  • CI: CodeRabbit レビューは進行中 (pending)。それ以外の status check は現時点で見当たらず、GitHub Actions CI の failure は無い
  • レビュー状況: CodeRabbit — レビュー未着手 (「processing new changes」のプレースホルダーコメントのみ、findings 無し)。人間レビューア — 未着手 (reviews 0 件、review_decision なし)
  • Verdict: approved (現時点で findings が 1 件も無いため。CodeRabbit 未着のため暫定であり、次の分析で再評価が必要)

Applicable Findings (Critical / High / Major)

(該当なし — レビュー指摘なし)

Applicable Findings (Medium 以下)

(該当なし)

Filtered (not applicable)

(該当なし)

差分概要 (レビュー指摘が無いための軽量サマリー)

diff stage (cli-push-runner/src/stages/diff.rs) の subprocess 呼び出しに timeout を追加する修正 (push パイプライン改善 T6)。変更ファイル 6件:

  • src/cli-push-runner/src/stages/diff.rsrun_diff_cmd を無限待ちの Command::output() から spawn + drain_pipe_unlimited + wait_with_timeout_safe に置き換え、既定 60s の timeout で fail-closed (exit 5) にする実装変更。回帰テスト 7 本追加 (mod t6_diff_timeout)
  • src/cli-push-runner/src/config/mod.rsDiffConfig.timeout: Option<u64> フィールドと DEFAULT_DIFF_TIMEOUT_SECS 追加、テスト 2 本追加
  • push-runner-config.toml / templates/push-runner-config.toml[diff] timeout の説明コメント追加のみ
  • docs/adr/adr-044-subprocess-utility-extraction-boundary.md / docs/push-pipeline-fix-plan.md — T6 の設計判断・実施結果の記録追記 (ADR-044 境界基準の適用例、および run_cmd_shell_* 3 variant 共通の timeout 欠陥を発見した旨を backlog 10 として記録)

次のアクション

  • CodeRabbit のレビュー完了を待ち、次回の分析 (新規コメント/レビュー発生時) で findings の有無を再評価する
  • diff.rs の変更は subprocess の child/grandchild lifecycle (cmd /c 経由の孫プロセスが kill() を生き延びる問題) に関わる繊細な修正のため、人間レビューでも該当箇所 (diff.rs:296-303 のコメント記載の join 回避ロジック) を重点確認することを推奨
  • push-runner-config.toml に記載の PR 番号「未採番」は、本 PR (fix(cli-push-runner): diff stage の timeout 欠落を修正 (push パイプライン改善 T6) #283) 採番後の backfill 漏れがないか確認すること

@aloekun
aloekun merged commit 69607a8 into master Jul 17, 2026
1 check passed
@aloekun
aloekun deleted the fix/diff-stage-timeout branch July 17, 2026 05:58
aloekun added a commit that referenced this pull request Jul 17, 2026
* docs(push-pipeline-fix-plan): T6 の PR 番号を #283 に backfill

T6 の作業コミット時点では PR が未採番だったため、計画書の §4 実施結果と §8
判定記録に「PR 未採番 — 採番後に backfill」と書いて負債を明示していた。PR #283
がマージされたため採番情報のみを更新する。

変更 (3 箇所、いずれも採番情報のみ):
- §4 T6 実施結果の見出し: PR 未採番 → PR #283
- §4 T6 の backlog 10 への申し送り: 「本 PR では触れず」→「PR #283 では触れず」
  (T5 が §4/§8 の「本 PR」を番号へ解決した慣習に揃える)
- §8 判定記録の T6 行: PR 未採番 → PR #283

由来: PR #282 (T5) の post-PR レビューで「T4 行が『本 PR』のまま放置され PR #282
で backfill する羽目になった」負債が指摘され、同じ形を繰り返さないために T6 では
未採番であることを明示していた。本コミットでその明示を回収する。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hooks-stop-quality): 品質ゲートの cwd 依存を修正 (push パイプライン改善 T7)

Stop hook はセッションの cwd を継承して起動されるため、cwd がリポジトリルート
以外 (例: .takt/runs に cd したまま Stop) だと 2 つの症状が黙って出ていた:

1. hooks-config.toml のルート相対 step (file-length) が「指定されたパスが
   見つかりません」で失敗し、品質ゲートが誤 block する (2026-07-16 に実発火)。
   pnpm / cargo 系 step は設定ファイルを上方探索するため偶然通っており、
   ルート相対パスを書いた step だけが壊れる非対称が発見を遅らせていた。
2. takt subsession 判定が <cwd>/.takt/runs を探して空振りし、active run を
   検出できない。ADR-004 の edit:false subsession skip が効かなくなる。

同一の根本原因なので main 冒頭で cwd を 1 度だけプロジェクトルートへ正規化する。
ルートは exe パス (<root>/.claude/<hook>.exe) から導出する — CLAUDE_PROJECT_DIR
env は VSCode 拡張環境で空になることを実測確認した (ADR-005 の不安定性が現存)。
config / pipeline lock / telemetry が既に採る exe-relative 規約と同形。

ルート特定不能時は警告のみで継続 (fail-open、pipeline_is_running と同じ線引き)。
main.rs が 800 行上限に触れたため takt 判定を takt_subsession.rs へ分離した。

回帰テスト: tests/t7_cwd_independence.rs に E2E 5 本 + unit 2 本 (26 → 33)。
exe を <root>/.claude/ に staging して spawn し、exe-relative 解決を実配置で
検証する。正規化の呼び出しを外すと bad 2 本が失敗し good 3 本は通ることを確認済み。

* fix(review): apply CodeRabbit fixes for #284

Resolved findings:
- [Major] src/hooks-stop-quality/tests/t7_cwd_independence.rs:96 hook の終了ステータスを確認してください

* fix(review): CodeRabbit 指摘の stderr 出力を補完 (#284)

auto-fix が追加した assert_hook_success は exit code assert 自体は入れたが、
指摘の「失敗時は stderr を出す」部分が未達だった: メッセージに stdout を渡しており、
かつ stderr.join() より前に呼ばれるため構造上 stderr を出せない。

本 hook の診断 (cwd 正規化の警告等) は eprintln! = stderr にしか出ないため、
指摘が想定する「非 0 exit かつ stdout が空」の失敗では stderr だけが手掛かりになる。
stderr を join してから assert に渡す形へ補正した。

guard が空振りでないことを実証済み: staged exe を where.exe (非 0 exit・stdout 空) に
差し替えると 5 本すべてが exit code Some(2) で失敗する (guard 導入前なら None を
期待する 3 本が false green で素通りしていた)。

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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