Skip to content

fix(post-merge-feedback): timeout reconciliation + 並行起動 guard + 並列化#79

Merged
aloekun merged 1 commit intomasterfrom
fix/adr-030-timeout-reconciliation
Apr 26, 2026
Merged

fix(post-merge-feedback): timeout reconciliation + 並行起動 guard + 並列化#79
aloekun merged 1 commit intomasterfrom
fix/adr-030-timeout-reconciliation

Conversation

@aloekun
Copy link
Copy Markdown
Owner

@aloekun aloekun commented Apr 26, 2026

Summary

Phase B dogfood (PR #77 / #78) で発見されたタイムアウト経路の残存不具合と、ボトルネック分析で判明した sequential 構造を併せて修正する。

主な観測:

  • PR fix(post-merge-feedback): task labeling 規約導入で run dir 検索を決定論化 #78 merge 後の post-merge-feedback workflow で child.kill() が takt の descendants を殺せず、kill 後 2m13s で feedback-report.md が生成 → .failed marker が誤情報になっていた
  • workflow が sequential chain で 12m13s (parallel 化想定 ~7m30s)
  • .takt/post-merge-feedback-context.json の cross-invocation overwrite race が orphan + 新規起動の組み合わせで成立する

修正内容

1. workflow を並列化 (.takt/workflows/post-merge-feedback.yaml)

3 つの analyze facet (analyze-pr / analyze-session / analyze-prepush-reports) を parallel: block で並列実行。既存の pre-push-review.yaml パターンに踏襲。total = max(analyze) + aggregate に短縮。

2. TAKT_TIMEOUT_SECS: 600s → 1200s (feedback.rs:42)

並列化想定 ~7m30s に対し 2x の安全係数。docstring に band-aid であることと、本質解は workflow 構造で時間を抑える方針を明記。

3. caller-side reconciliation (feedback.rs:836-877)

run_takt_workflow の戻り値に関わらず copy_feedback_report を必ず試行:

let takt_ok = run_takt_workflow(...);
match copy_feedback_report(...) {
    Ok(report) => {
        cleanup_failed_marker(...);
        Ok(report)
    }
    Err(copy_err) => Err(format!("{}: {}", cause, copy_err))
}

Windows の child.kill() は takt の descendants を殺せず orphan として走り続けるため、Rust が timeout した後でも report が完成するケースを救済。既存の .failed marker は cleanup する。

4. 並行起動 guard (feedback.rs:787-820)

CONCURRENT_RUN_GUARD_SECS = 1500 (timeout 1200s より少し長い) を導入。run() 冒頭で .takt/post-merge-feedback-context.json の経過時刻を確認し、TTL 内なら refuse:

前回の post-merge-feedback workflow がまだ進行中の可能性
(context.json が <N>s 前に書かれた)。<TTL>s 待つか、進行中の takt が
無いことを確認してから手動で <path> を削除してください。

cross-invocation race を発生源で予防。

5. テスト追加 (5 件)

  • concurrent_run_guard_passes_when_context_absent
  • concurrent_run_guard_blocks_when_context_recent
  • concurrent_run_guard_passes_when_context_stale
  • context_age_secs_returns_none_for_missing
  • context_age_secs_returns_some_for_existing

6. ADR-030 更新 (docs/adr/adr-030-deterministic-post-merge-feedback.md)

ボトルネック分析の再掲

PR #78 trace.md から step ごとの実時間:

step 所要時間
analyze-pr 3m 22s
analyze-session 5m 24s ← 最長 (transcript 量に依存)
analyze-prepush-reports 1m 21s
aggregate-feedback 2m 06s
sequential 合計 12m 13s
parallel 想定 ~7m 30s

並列化により ~4m 43s 短縮。

残存リスク (Phase B 外)

  • Windows job object 化: child.kill() で descendants まで kill する根本対策。Plan B 相当だが工数大、reconciliation で実用上カバーできるため後回し
  • 完全な per-invocation isolation: takt の context dir 連携で .takt/post-merge-feedback-context.json の race を構造的に消す。Phase C 以降に検討
  • analyze-session の transcript 依存スケール: 長期 PR で再びタイムアウトするリスク。filter 戦略の改善 (発言数の cap、tool call 選別等) で対応可能

Test plan

  • cargo test workspace 全 green (cli-merge-pipeline 44 tests、5 件新規)
  • cargo clippy --workspace -- -D warnings 通過
  • cargo fmt --check 通過
  • release build (cli-merge-pipeline): 成功
  • pnpm exec takt prompt post-merge-feedback: 並列化後の workflow が parse 成功 (Step 1 = analyze parallel, Step 2 = aggregate-feedback)
  • pre-push-review pipeline (takt) approve

dogfood 検証: 本 PR マージ後の post-merge-feedback workflow が parallel 構成で ~7m30s 以内に完了し、.claude/feedback-reports/<pr>.md を正しく生成することを確認する。

References

Summary by CodeRabbit

リリースノート

  • New Features

    • フィードバック分析処理の並列実行に対応し、処理時間を短縮
    • 同時実行保護機能を実装
  • Bug Fixes

    • ワークフロー失敗時の復旧処理を改善
  • Documentation

    • パフォーマンスモデルとシステムの動作仕様に関する文書を更新

Phase B dogfood で発見されたタイムアウト経路の残存不具合と、ボトルネック分析で
判明した sequential 構造を併せて修正する。

## 主な変更

### 1. workflow の並列化 (.takt/workflows/post-merge-feedback.yaml)
3 つの analyze facet (analyze-pr / analyze-session / analyze-prepush-reports) は
独立した情報源を扱うため parallel block で並列実行する。
PR #78 計測 (sequential 12m13s) → parallel 想定 ~7m30s に短縮。

### 2. TAKT_TIMEOUT_SECS: 600s → 1200s
PR #77 (14m21s)、#78 (12m13s) 観測実績を踏まえた暫定値。並列化後の想定 7m30s
に対し 2x の安全係数。docstring に「band-aid」と明記し、本質解は workflow 構造で
時間を抑えることだと残した。

### 3. caller-side reconciliation (run() 末尾)
Windows の child.kill() が takt の descendants を殺せず、Rust が timeout で kill
した後も takt が orphan として走り続けて report を完成させるケースを観測した
(PR #78 で kill 後 2m13s で feedback-report.md 完成)。

run_takt_workflow の戻り値に関わらず copy_feedback_report を必ず試行し、report が
存在すれば success 扱いとする。既存の .failed marker は cleanup_failed_marker で
除去する。

### 4. 並行起動 guard (CONCURRENT_RUN_GUARD_SECS = 1500s)
cross-invocation context overwrite race を発生源で予防。
.takt/post-merge-feedback-context.json が 1500s 以内に書かれていれば、orphan takt
が走行中の可能性が高いとみなして新規実行を refuse する。

### 5. テスト追加 (5 件)
concurrent_run_guard / context_age_secs の振る舞いを検証。

### 6. ADR-030 更新
- レイテンシモデル (parallel 想定 = max(analyze) + aggregate)
- Reconciliation 設計の根拠
- 並行起動 guard の意図と Phase B 外切り出し方針

## 残存リスク (Phase B 外)
- Windows の job object でプロセスツリー全体を kill する根本対策
- 完全な per-invocation isolation (takt context dir 連携)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

ウォークスルー

ワークフローが順序実行から並列実行に変更され、3つの分析ファセット(PR分析、セッション分析、プリプッシュレポート分析)が同時実行されるようになりました。フィードバック生成コードにはタイムアウト値が600秒から1200秒に引き上げられ、並行実行ガード機構と調和ロジックが追加されました。

変更内容

コホート / ファイル(s) 概要
ワークフロー構造の再設計
.takt/workflows/post-merge-feedback.yaml
順序チェーン(analyze-pranalyze-sessionanalyze-prepush-reports)を単一の analyze ステップ内の parallel ブロックに変更し、3つの分析を同時実行。ルーティングロジックを調整し、すべての並列ファセットが完了時のみ aggregate-feedback に進むように更新。
動作仕様の文書化
docs/adr/adr-030-deterministic-post-merge-feedback.md
Phase Bの動作調整を記載:run_takt_workflow 失敗後の copy_feedback_report 無条件再試行、.takt/post-merge-feedback-context.json の経過時間による並行実行ガード(1500秒TTL)、並列実行の遅延モデルと1200秒のタイムアウト値を追加。
エラーハンドリングと並行制御
src/cli-merge-pipeline/src/feedback.rs
TAKT_TIMEOUT_SECS を600秒から1200秒に増加。CONCURRENT_RUN_GUARD_SECS (1500秒)の新しい定数を追加し、既存コンテキストの経過時間をチェック。run_takt_workflow 失敗後も copy_feedback_report による調和処理を実施し、成功時は失敗マーカーをクリア。複数のテストケースを追加。

シーケンス図

sequenceDiagram
    participant Workflow as ワークフロー<br/>オーケストレータ
    participant AnalyzePR as PR分析ステップ
    participant AnalyzeSession as セッション分析<br/>ステップ
    participant AnalyzePrepush as プリプッシュ分析<br/>ステップ
    participant Aggregate as フィードバック<br/>集約ステップ
    
    rect rgba(100, 150, 200, 0.5)
    Note over Workflow,AnalyzePrepush: 新しい並列実行フロー
    end
    
    Workflow->>AnalyzePR: 分析開始(並列)
    Workflow->>AnalyzeSession: 分析開始(並列)
    Workflow->>AnalyzePrepush: 分析開始(並列)
    
    par 並列実行
        AnalyzePR-->>Workflow: 分析完了
    and
        AnalyzeSession-->>Workflow: 分析完了
    and
        AnalyzePrepush-->>Workflow: 分析完了
    end
    
    Workflow->>Aggregate: すべての結果を使用して集約
    Aggregate-->>Workflow: フィードバックレポート生成
Loading

推定コードレビュー工数

🎯 3 (中程度) | ⏱️ ~20分

関連する可能性のあるPR

  • PR #77: メインPRが同じポストマージフィードバックワークフローと feedback.rs の動作(タイムアウト、実行オーケストレーション、調和/並行ガード)を修正・拡張しており、直接的な関連がある
  • PR #75: メインPRが導入済みのADR-030を更新・拡張し、同じADRドキュメントを修正して、そこで記述されたtakt/ワークフロー動作を実装している
🚥 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 プルリクエストのタイトルは、主要な3つの変更(タイムアウト調整、並行起動ガード、並列化)を正確に要約しており、変更セットの中心的な内容を明確に反映しています。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
src/cli-merge-pipeline/src/feedback.rs (4)

432-435: SystemTime::elapsed() は将来 mtime(clock skew)で Err を返し guard を素通りさせる点に留意。

fs::metadata(...).modified() が NTFS / 時刻同期巻き戻し等で「未来の mtime」を返した場合、modified.elapsed()SystemTimeErrorNone 化 → check_concurrent_run_guardOk(()) を返し、orphan が走っていても guard を通します。Windows-local 限定運用(時刻同期前提)なので実害は低いと思いますが、保守的にやるなら elapsed() 失敗時は age=0 扱いで guard を効かせる選択肢もあります。

♻️ 防御的な代替実装の例
 fn context_age_secs(context_path: &Path) -> Option<u64> {
-    let modified = fs::metadata(context_path).ok()?.modified().ok()?;
-    modified.elapsed().ok().map(|d| d.as_secs())
+    let modified = fs::metadata(context_path).ok()?.modified().ok()?;
+    // 未来 mtime (clock skew) は age=0 として guard を効かせる側に倒す
+    Some(modified.elapsed().map(|d| d.as_secs()).unwrap_or(0))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-merge-pipeline/src/feedback.rs` around lines 432 - 435,
context_age_secs currently returns None when modified.elapsed() errors (e.g.
future mtime due to clock skew), which lets check_concurrent_run_guard skip the
guard; change context_age_secs (the function calling
fs::metadata(...).modified()) to treat elapsed() errors defensively by returning
Some(0) (or otherwise clamping to 0) instead of None so a failed elapsed()
enforces the guard rather than bypassing it.

848-865: stale ケースの実挙動が未検証。

concurrent_run_guard_passes_when_context_stale はコメント通り、CONCURRENT_RUN_GUARD_SECS が const のため stale 経路を直接通せず context_age_secs の存在確認のみに留まっています。check_concurrent_run_guardage >= CONCURRENT_RUN_GUARD_SECS → Ok(()) 分岐そのものは現状ノーカバーです。filetime クレートを足すか、guard ロジックを (context_path, ttl_secs) 受け取りの薄いヘルパーに切り出して TTL=0 の単体テストを通すと、命名どおりの保証になります。

♻️ ヘルパー切り出し案
-fn check_concurrent_run_guard(context_path: &Path) -> Result<(), String> {
-    let Some(age) = context_age_secs(context_path) else {
-        return Ok(());
-    };
-    if age >= CONCURRENT_RUN_GUARD_SECS {
-        return Ok(());
-    }
-    Err(format!(
+fn check_concurrent_run_guard_with_ttl(
+    context_path: &Path,
+    ttl_secs: u64,
+) -> Result<(), String> {
+    let Some(age) = context_age_secs(context_path) else {
+        return Ok(());
+    };
+    if age >= ttl_secs {
+        return Ok(());
+    }
+    Err(format!(
         "前回の post-merge-feedback workflow がまだ進行中の可能性 \
-         (context.json が {}s 前に書かれた)。{}s 待つか、進行中の takt が無いことを\
+         (context.json が {}s 前に書かれた)。{}s 待つか、進行中の takt が無いことを\
          確認してから手動で {} を削除してください。",
         age,
-        CONCURRENT_RUN_GUARD_SECS,
+        ttl_secs,
         context_path.display()
     ))
 }
+
+fn check_concurrent_run_guard(context_path: &Path) -> Result<(), String> {
+    check_concurrent_run_guard_with_ttl(context_path, CONCURRENT_RUN_GUARD_SECS)
+}

これで check_concurrent_run_guard_with_ttl(&path, 0) を使った stale 通過テストが書けます。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-merge-pipeline/src/feedback.rs` around lines 848 - 865, The test
currently doesn't exercise the stale branch; extract the TTL-specific logic into
a thin helper (e.g. check_concurrent_run_guard_with_ttl(context_path: &Path,
ttl_secs: u64) -> Result<(), Error>) that implements the age >=
CONCURRENT_RUN_GUARD_SECS check using the passed ttl_secs, refactor existing
check_concurrent_run_guard to call this helper with the constant
CONCURRENT_RUN_GUARD_SECS, and add a unit test that calls
check_concurrent_run_guard_with_ttl(&path, 0) to assert it returns Ok(()) (or
otherwise succeeds) to cover the stale-path behavior; alternatively, if you
prefer file timestamp manipulation, add the filetime crate and modify the test
to set mtime old enough and then call the original check_concurrent_run_guard to
validate the stale branch.

442-457: 並行起動 guard が「正常完了直後の連続 merge」も最大 25 分ブロックする副作用に注意。

check_concurrent_run_guardcontext.json の mtime のみを基準にしているため、orphan 残存の有無を区別できません。前回 run が 正常完了 していても context.json は run 開始時の mtime のままで、CONCURRENT_RUN_GUARD_SECS = 1500s が経過するまで次の pnpm merge-pr がエラー終了します。Phase B のスコープ外とは承知していますが、

  • 正常完了した経路 (copy_feedback_report 成功 + cleanup_failed_marker) で context.json も削除 / マーカー化する、
  • もしくは <pr>.md / <pr>.md.failed の存否との AND で判定する、

といった軽い改善で 25 分ロックの副作用は緩和できます。エラーメッセージにも「進行中の takt が無い場合は context.json を削除して再実行してください」と既に書かれているので、Phase C 以降で検討可能ならそれで十分です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-merge-pipeline/src/feedback.rs` around lines 442 - 457,
check_concurrent_run_guard currently only checks context.json mtime (via
context_age_secs) which causes a finished run to still block new runs for
CONCURRENT_RUN_GUARD_SECS; modify the flow so completed runs remove or mark
context.json (in the success path that calls copy_feedback_report and
cleanup_failed_marker) or change check_concurrent_run_guard to treat a run as
finished when context.json is old OR when there is no active marker by also
checking for the existence of the per-PR output files (e.g., "<pr>.md" or
"<pr>.md.failed"); update references to check_concurrent_run_guard,
context_age_secs, copy_feedback_report, and cleanup_failed_marker accordingly so
successful completion either deletes/flags context.json or the guard uses an
AND/OR check with "<pr>.md"/"<pr>.md.failed" presence to avoid blocking normal
consecutive merges.

896-903: guard を fetch_pr_time_range より前に移すと余計な gh 呼び出しを避けられます。

現状は fetch_pr_time_range (gh API 呼び出し) が先に走り、その後で check_concurrent_run_guard が refuse する可能性があります。並行起動を弾く判定には PR 時刻 range は不要なので、guard を最初に持って来ると無駄な外部 I/O とエラーパスが減ります。最適化レベルの提案です。

♻️ 並び替え案
 pub fn run(input: &FeedbackInput) -> Result<PathBuf, String> {
-    let range = fetch_pr_time_range(input.pr_number, input.owner_repo)
-        .map_err(|e| format!("PR 時刻 range 取得失敗: {}", e))?;
-
     let context_path = input.repo_root.join(CONTEXT_PATH);
     let transcript_path = input.repo_root.join(TRANSCRIPT_PATH);
 
     check_concurrent_run_guard(&context_path)?;
+
+    let range = fetch_pr_time_range(input.pr_number, input.owner_repo)
+        .map_err(|e| format!("PR 時刻 range 取得失敗: {}", e))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-merge-pipeline/src/feedback.rs` around lines 896 - 903, Move the
concurrent-run guard check to the start of run to avoid unnecessary GH API
calls: call check_concurrent_run_guard(&context_path) immediately after
computing context_path (or compute context_path first) and return early on
failure, then call fetch_pr_time_range(input.pr_number, input.owner_repo)
afterwards; update run to compute context_path/transcript_path, invoke
check_concurrent_run_guard before fetch_pr_time_range, and keep existing error
mapping logic unchanged for fetch_pr_time_range.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/cli-merge-pipeline/src/feedback.rs`:
- Around line 432-435: context_age_secs currently returns None when
modified.elapsed() errors (e.g. future mtime due to clock skew), which lets
check_concurrent_run_guard skip the guard; change context_age_secs (the function
calling fs::metadata(...).modified()) to treat elapsed() errors defensively by
returning Some(0) (or otherwise clamping to 0) instead of None so a failed
elapsed() enforces the guard rather than bypassing it.
- Around line 848-865: The test currently doesn't exercise the stale branch;
extract the TTL-specific logic into a thin helper (e.g.
check_concurrent_run_guard_with_ttl(context_path: &Path, ttl_secs: u64) ->
Result<(), Error>) that implements the age >= CONCURRENT_RUN_GUARD_SECS check
using the passed ttl_secs, refactor existing check_concurrent_run_guard to call
this helper with the constant CONCURRENT_RUN_GUARD_SECS, and add a unit test
that calls check_concurrent_run_guard_with_ttl(&path, 0) to assert it returns
Ok(()) (or otherwise succeeds) to cover the stale-path behavior; alternatively,
if you prefer file timestamp manipulation, add the filetime crate and modify the
test to set mtime old enough and then call the original
check_concurrent_run_guard to validate the stale branch.
- Around line 442-457: check_concurrent_run_guard currently only checks
context.json mtime (via context_age_secs) which causes a finished run to still
block new runs for CONCURRENT_RUN_GUARD_SECS; modify the flow so completed runs
remove or mark context.json (in the success path that calls copy_feedback_report
and cleanup_failed_marker) or change check_concurrent_run_guard to treat a run
as finished when context.json is old OR when there is no active marker by also
checking for the existence of the per-PR output files (e.g., "<pr>.md" or
"<pr>.md.failed"); update references to check_concurrent_run_guard,
context_age_secs, copy_feedback_report, and cleanup_failed_marker accordingly so
successful completion either deletes/flags context.json or the guard uses an
AND/OR check with "<pr>.md"/"<pr>.md.failed" presence to avoid blocking normal
consecutive merges.
- Around line 896-903: Move the concurrent-run guard check to the start of run
to avoid unnecessary GH API calls: call
check_concurrent_run_guard(&context_path) immediately after computing
context_path (or compute context_path first) and return early on failure, then
call fetch_pr_time_range(input.pr_number, input.owner_repo) afterwards; update
run to compute context_path/transcript_path, invoke check_concurrent_run_guard
before fetch_pr_time_range, and keep existing error mapping logic unchanged for
fetch_pr_time_range.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3149b269-5c25-47d2-8362-c865fc1edce0

📥 Commits

Reviewing files that changed from the base of the PR and between 130790a and fef5fdd.

📒 Files selected for processing (3)
  • .takt/workflows/post-merge-feedback.yaml
  • docs/adr/adr-030-deterministic-post-merge-feedback.md
  • src/cli-merge-pipeline/src/feedback.rs

@aloekun aloekun merged commit 72a1f47 into master Apr 26, 2026
1 check passed
@aloekun aloekun deleted the fix/adr-030-timeout-reconciliation branch April 26, 2026 03:57
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