MF Blogs 便利ツール
複数の処理が同じ状態へ同時にアクセスする並行処理の抽象図

記事

Claude Codeでレースコンディションを疑うときの調査プロンプト

「たまに失敗する」非同期バグをClaude Codeで調査するときの依頼文テンプレートを整理します。再現条件の作り方、タイムスタンプ付きログの仕込み方、共有状態の特定、修正案の出させ方、ストレステストでの検証までを公式仕様と現場運用の両面でまとめた、レースコンディションを疑う上級者向けの実務ガイドです。

0:00 0:00

Claude Codeで「たまにテストが落ちる」「本番でまれにデータがずれる」という非同期バグを調べるとき、エラーメッセージを貼って「直して」と頼んでも解決しません。レースコンディションは再現できなければ直したかどうかも判定できないからです。本記事は、再現条件を作ってから依頼するための調査プロンプトの型を整理します。

結論:再現できないバグは「再現させてから」依頼する

レースコンディションは、複数の処理が同じ状態に順序保証なくアクセスすることで起きます。実行のたびに結果が変わるため、1回のエラーログだけ渡しても、Claude Codeは推測でそれらしい修正を書くだけになります。

Anthropic公式のbest practicesも、AIの作業は「verify its work(検証する)」べきだと強調しています。レースコンディションの調査は、次の順序を人間が主導します。

  1. 再現条件を作る:確率的にしか起きないバグを、高確率で起きる状態に追い込む
  2. 観測手段を仕込む:タイムスタンプと実行単位IDつきのログ
  3. 仮説を出させる:どの共有状態が、どの順序で壊れるか
  4. 修正案を出させる:仮説1つずつ、最小修正で
  5. ストレステストで検証する:修正前に落ち、修正後に落ちないことを数百回で確認

「再現→観測→仮説→修正→検証」のうち、AIに任せていいのは仮説と修正案の下書きだけです。再現と検証は人間が手元で回します。

ステップ1:再現条件を作る

確率的なバグは、次のいずれかで発生率を上げられます。これは人間がやる作業です。

手法何をするか
反復実行同じテストを数百〜数千回ループで回す
並行数を上げる同時実行するワーカー・リクエスト数を増やす
人工的な遅延疑わしい処理の前後にsleepやランダム遅延を挿入
リソース制限CPUコア数を絞る、または逆に増やしてスケジューリングを変える

たとえばテストを繰り返し回して、落ちる回数を数えるのは次のようにできます。

for i in $(seq 1 500); do npm test -- --runInBand path/to/flaky.test.ts || echo 'FAIL at '$i; done

落ちる頻度(例:500回中12回)を数値で押さえることが重要です。この数値が、修正後に0回になったかを判定するベースラインになります。再現率が低すぎる場合は、人工遅延を入れて窓を広げます。

ステップ2:観測手段を仕込む

レースコンディションは「どの順序で起きたか」が分からないと特定できません。次の3つを含むログを仕込みます。

  • 高精度タイムスタンプ:ミリ秒またはナノ秒
  • 実行単位の識別子:スレッドID、非同期タスクID、リクエストID
  • 対象の状態:読み書きした共有変数・行・キーの値

この「ログを仕込む」作業はClaude Codeに依頼できます。依頼文の例です。

目的: レースコンディション調査のための観測ログを追加する

対象ファイル: @src/services/counter.ts

依頼:
- 共有状態(balance、cache、セッションマップ)を読み書きする箇所すべてに
  デバッグログを追加する
- 各ログに「ISO8601のミリ秒タイムスタンプ」「リクエストID」
  「操作種別(read/write)」「対象キー」「値」を含める
- ログは環境変数 DEBUG_RACE=1 のときだけ出力する

完了条件:
- 既存テストが全て緑のまま
- ログ追加以外の挙動変更がない

禁止事項:
- ロジックの変更や「ついでの修正」
- ログを常時出力にすること

挙動を変えずにログだけ足すのが要点です。修正と観測を混ぜると、どちらが効いたのか分からなくなります。

ステップ3:仮説を出させる

再現条件とログが揃ったら、Plan Modeで実装させずに仮説を出させます。Plan Modeは読み取り系ツールだけを実行するため、調査に向いています。

症状: src/services/counter.ts の incrementBalance を
500回並行実行すると、最終値がまれに期待値より小さくなる(500回中12回再現)

再現手順:
- DEBUG_RACE=1 でストレステストを実行
- 添付ログ race.log にタイムスタンプ付きの read/write 記録がある

関連ファイル:
@src/services/counter.ts
@src/services/cache.ts

依頼:
1. このログから、どの共有状態が、どの2つの操作の間で
   順序保証なく競合しているか仮説を3つ挙げる
2. 各仮説について「競合する読み書きのペア」「壊れる順序の具体例」
   「検証方法」「影響範囲」を書く
3. 実装はしない。Plan Modeで止める

禁止事項:
- ログにない動作の推測で断定すること
- 「とりあえずロックを足す」式の修正提案

ログをclaude -pにパイプで渡すと、巨大ログでもコンテキストを圧迫しにくくなります。

grep -E 'write|read' race.log | claude -p '添付ログから競合している読み書きのペアを時系列で抽出して'

ステップ4:修正案を1つずつ出させる

仮説が固まったら、1仮説ずつ修正案を依頼します。レースコンディションの代表的な対処は次のとおりですが、どれを使うかは仮説によって変わります。

対処向く状況
不可分操作(atomic)にする単一のカウンタ・フラグの更新
ロック・ミューテックス複数ステップの更新をまとめて保護したい
直列化(キュー化)並行で動く必要がない処理
楽観ロック(バージョン番号・CAS)DB行など、競合がまれな更新
状態の共有をやめるそもそも共有しない設計に変える

依頼文には「触ってよいファイル」「公開APIを変えない」「ロックの粒度を最小にする」を明示します。安易に広い範囲をロックすると、デッドロックや性能劣化という別の問題を呼びます。修正の進め方そのものは「Claude Codeで既存コードを壊さずリファクタリングする依頼文」の型がそのまま使えます。

ステップ5:ストレステストで検証する

レースコンディションの修正は、通常のテスト1回では検証になりません。ステップ1の再現条件で、修正前に落ち、修正後に落ちないことを確認します。

検証を依頼に組み込むときは、失敗テストを先に書かせます。「Claude Codeでテスト駆動開発をする手順」のバグ修正TDDと同じ流れです。

依頼:
1. incrementBalance を高並行で叩き、最終値が期待値とずれたら
   失敗するストレステストを追加する(並行500、反復5回)
2. このテストが「現状のコードで落ちる」ことを先に見せる
3. 人間の確認を待つ。OKが出てから修正に進む

完了条件:
- 修正前: 追加テストが落ちる
- 修正後: 追加テストが500回×5反復で1回も落ちない
- 既存テストが全て緑

注意点として、ストレステストが「たまたま通った」可能性は残ります。再現率が元々低い場合、検証の反復回数は再現率から逆算して十分に大きく取ります。

やってはいけないこと

  • エラーログ1回分だけ渡して「直して」と頼む:再現条件がないと検証できない
  • 仮説を飛ばしていきなりロックを入れる:デッドロックや性能劣化を招く
  • 複数の修正案を同時に試す:どれが効いたか分からなくなる
  • sleepを入れて「直った」とする:タイミングをずらしただけで、根本原因は残る
  • 観測ログを本番に出しっぱなしにする:性能と情報漏えいのリスク。秘密情報の扱いは「Claude Codeに秘密情報を渡さないための実践ルール」を参照

チェックリスト

  • 確率的なバグを高確率で再現する条件を作った
  • 落ちる頻度を数値(例:500回中12回)で押さえた
  • タイムスタンプ・実行単位ID・対象状態を含むログを仕込んだ
  • 観測ログの追加と修正を別ステップに分けた
  • Plan Modeで仮説を3つ出させ、競合ペアを特定した
  • 修正案は1仮説ずつ、ロック粒度を最小にして出させた
  • 失敗するストレステストを先に書かせ、人間が確認した
  • 修正後に十分な反復回数で落ちないことを検証した

次に読むおすすめ記事: