mcpField
R
race-conditions

race-conditions

github.com/yanko-belov開発1346 回インストール40 週間前に更新
リポジトリ

race-conditions について

タイミングが正しさに影響する競合状態を見つけ、対処する開発者向けスキルです。検出パターンに加え、アトミック操作、トランザクション、ロック、冪等性キーを扱います。

主な機能

  1. 01確認後に実行する競合パターンの特定
  2. 02アトミック更新とDBトランザクションの説明
  3. 03楽観ロックと分散ロックの紹介
  4. 04冪等性キーによる重複処理の防止
  5. 05並行テストの考え方

活用シーン

  1. 01断続的で再現しにくい不具合の調査
  2. 02DBの読み取り後の書き込み処理の検討
  3. 03共有状態や並行APIリクエストの点検

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yanko-belov/code-craft --skill race-conditions

インストール後、Claude Code で/race-conditionsを入力して読み込まれたか確認します。

詳細

競合状態

概要

結果がタイミングによって変わるなら、それは競合状態です。競合状態は、いつ発生してもおかしくないバグです。

競合状態は、正しさがイベントの相対的なタイミングに依存する場合に発生します。ほとんどの場合は問題なく動作する一方で、ランダムに失敗し、再現がほぼ不可能なため、厄介です。

使用する状況

  • 複数の非同期処理が共有状態にアクセスする
  • データベースの読み取り後に書き込むパターン
  • 同時に実行される API リクエストが同じリソースを変更する
  • 「開発環境では動くが、本番環境では失敗する」
  • 再現できない断続的なバグ

絶対原則

NEVER read-then-write without atomicity guarantees.

例外はありません。

  • 「処理が速いから、タイミングは関係ない」場合でも
  • 「一度にユーザーは 1 人だけだから」場合でも
  • 「壊れたら修正すればいい」場合でも
  • 「テストでは動く」場合でも

タイミングが結果に影響する可能性があるなら、競合状態が発生しています。

検出:TOCTOU パターン

Time-Of-Check to Time-Of-Use:何かを確認してから、それに基づいて処理を行うことです。

// ❌ VIOLATION: Classic race condition
async function withdrawMoney(accountId: string, amount: number): Promise<void> {
  // Time-of-check
  const account = await db.accounts.findById(accountId);
  if (account.balance >= amount) {
    // Time-of-use (gap where another transaction can happen!)
    await db.accounts.update(accountId, {
      balance: account.balance - amount  // Uses stale value!
    });
  }
}

// Two simultaneous $80 withdrawals from $100 account:
// T1: Reads balance = $100 ✓
// T2: Reads balance = $100 ✓  (race!)
// T1: balance >= 80? Yes. Update to $20
// T2: balance >= 80? Yes. Update to $20  (should have been denied!)
// Result: Two $80 withdrawals, final balance $20 (should be overdraft error)

正しいパターン

1. アトミック操作
// ✅ CORRECT: Atomic update with condition
async function withdrawMoney(accountId: string, amount: number): Promise<boolean> {
  const result = await db.accounts.updateOne(
    { 
      _id: accountId,
      balance: { $gte: amount }  // Check AND update atomically
    },
    { 
      $inc: { balance: -amount } 
    }
  );
  
  if (result.modifiedCount === 0) {
    throw new InsufficientFundsError(accountId, amount);
  }
  
  return true;
}
2. データベーストランザクション
// ✅ CORRECT: Transaction with proper isolation
async function transferMoney(
  fromId: string, 
  toId: string, 
  amount: number
): Promise<void> {
  await db.transaction(async (tx) => {
    // Lock rows with FOR UPDATE
    const from = await tx.accounts
      .findById(fromId)
      .forUpdate();  // Locks row until commit
    
    const to = await tx.accounts
      .findById(toId)
      .forUpdate();
    
    if (from.balance < amount) {
      throw new InsufficientFundsError(fromId, amount);
    }
    
    await tx.accounts.update(fromId, { balance: from.balance - amount });
    await tx.accounts.update(toId, { balance: to.balance + amount });
    
    // Commit releases locks
  });
}
3. 楽観的ロック
// ✅ CORRECT: Optimistic locking with version
async function updateDocument(
  id: string, 
  updates: Partial<Document>
): Promise<Document> {
  const maxRetries = 3;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const doc = await db.documents.findById(id);
    
    const result = await db.documents.updateOne(
      { 
        _id: id,
        version: doc.version  // Only update if version matches
      },
      { 
        $set: updates,
        $inc: { version: 1 }  // Increment version
      }
    );
    
    if (result.modifiedCount > 0) {
      return { ...doc, ...updates, version: doc.version + 1 };
    }
    
    // Version mismatch - someone else updated, retry
    await sleep(Math.random() * 100);  // Jitter
  }
  
  throw new ConcurrentModificationError(id);
}
4. 分散ロック
// ✅ CORRECT: Distributed lock for complex operations
async function processOrder(orderId: string): Promise<void> {
  const lockKey = `order:${orderId}:lock`;
  const lockTTL = 30000; // 30 seconds
  
  const lock = await redis.acquireLock(lockKey, lockTTL);
  if (!lock) {
    throw new OrderAlreadyProcessingError(orderId);
  }
  
  try {
    // Safe - only one process can be here for this order
    await doExpensiveOrderProcessing(orderId);
  } finally {
    await redis.releaseLock(lockKey, lock);
  }
}
5. 冪等性キー
// ✅ CORRECT: Idempotency prevents duplicate processing
async function createPayment(
  idempotencyKey: string,
  data: PaymentData
): Promise<Payment> {
  // Check if already processed
  const existing = await db.payments.findByIdempotencyKey(idempotencyKey);
  if (existing) {
    return existing;  // Return previous result
  }
  
  // Try to claim the idempotency key atomically
  try {
    await db.idempotencyKeys.insert({
      key: idempotencyKey,
      status: 'processing',
      createdAt: new Date(),
    });
  } catch (error) {
    if (isDuplicateKeyError(error)) {
      // Another request claimed it - fetch and return
      const existing = await db.payments.findByIdempotencyKey(idempotencyKey);
      if (existing) return existing;
      throw new PaymentProcessingError('Payment in progress');
    }
    throw error;
  }
  
  // Safe to process - we own the idempotency key
  const payment = await processPayment(data);
  
  await db.idempotencyKeys.update(idempotencyKey, {
    status: 'completed',
    result: payment.id,
  });
  
  return payment;
}

よくある競合状態のパターン

パターン問題解決策
確認してから実行確認と実行の間に状態が変化する確認と実行をアトミックに行う
読み取り・変更・書き込み読み取り後に値が変化するアトミックな更新またはロック
遅延初期化複数のスレッドが初期化するダブルチェックロックまたはアトミックな初期化
カウンターの増分更新が失われるアトミックな増分
最初の 1 件を取得複数の処理が「最初」を取得する一意制約を使ったアトミックな取得

言語固有のパターン

JavaScript/Node.js
// ❌ VIOLATION: Shared state in closure
let requestCount = 0;
async function handleRequest() {
  requestCount++;  // Race! Read-modify-write is NOT atomic
  // ...
}

// ✅ CORRECT: Atomic counter
import { createClient } from 'redis';
const redis = createClient();

async function handleRequest() {
  const count = await redis.incr('request_count');  // Atomic
  // ...
}
Python
# ❌ VIOLATION: Check-then-act
def get_or_create(key: str, factory: Callable) -> Any:
    if key not in cache:  # Check
        cache[key] = factory()  # Act - race!
    return cache[key]

# ✅ CORRECT: Atomic with lock
from threading import Lock
lock = Lock()

def get_or_create(key: str, factory: Callable) -> Any:
    with lock:
        if key not in cache:
            cache[key] = factory()
        return cache[key]

プレッシャーへの対処プロトコル

1. 「処理が速いから、タイミングは関係ない」

プレッシャー:「処理にかかる時間はマイクロ秒です」

**回答:**本番環境の負荷によって処理が重なります。負荷が高いと、「速い」処理も頻繁に重なります。競合状態はトラフィックに応じて増加します。

**対応:**アトミック操作を使います。速度は競合状態を防ぎません。

2. 「一度にユーザーは 1 人だけ」

プレッシャー:「トラフィックが少ないので、リクエストは同時に発生しません」

**回答:**ユーザーはダブルクリックします。タブは更新されます。ボットは大量のリクエストを送ります。モバイル端末はタイムアウト時にリトライします。「トラフィックが少ない」場合でも、一時的に集中することがあります。

**対応:**同時実行を想定していない場合でも、同時実行に対応できるよう設計します。

3. 「壊れたら修正すればいい」

プレッシャー:「まずリリースして、修正は後で」

**回答:**競合状態は再現がほぼ不可能です。「ランダムな」障害のデバッグに何週間もかかることになります。

**対応:**最初から正しく構築します。後からデバッグするよりも低コストです。

4. 「テストでは動く」

プレッシャー:「テストはすべて合格しています」

**回答:**テストは順次実行されます。本番環境では同時に実行されます。逐次実行では競合状態が表面化しません。

**対応:**並行テストを作成します。負荷テストを実施します。競合状態があるものと考えます。

危険信号 — 作業を止めて再検討

次のパターンが 1 つでもあれば、競合状態が発生している可能性があります。

  • if (condition) { update based on condition }
  • read(); compute(); write(computed);
  • check availability; book;
  • get count; increment; save count;
  • グローバル状態または共有される可変状態
  • 「ほとんどの場合は動く」
  • 「開発環境では再現できない」
  • 断続的なバグを「解消する」タイムアウト

これらはすべて、アトミック性を保証する必要があることを示しています。

競合状態のテスト

// Concurrent test to expose races
describe('withdraw', () => {
  it('handles concurrent withdrawals correctly', async () => {
    await db.accounts.create({ id: 'test', balance: 100 });
    
    // Simulate 10 concurrent $20 withdrawals
    const withdrawals = Array(10).fill(null).map(() => 
      withdrawMoney('test', 20).catch(() => 'failed')
    );
    
    const results = await Promise.all(withdrawals);
    const successful = results.filter(r => r !== 'failed').length;
    
    // Only 5 should succeed (5 × $20 = $100)
    expect(successful).toBe(5);
    
    const account = await db.accounts.findById('test');
    expect(account.balance).toBe(0);
  });
});

よくある自己正当化(すべて誤り)

言い訳実態
「十分に速い」速い処理でも、負荷が高いと重なります。
「トラフィックが少ない」リトライ、ダブルクリック、ボットによって同時実行が発生します。
「開発環境では動く」開発環境では逐次実行されます。本番環境では並行実行されます。
「壊れたら修正する」競合状態のバグは見つけるのが困難です。今すぐ修正します。
「スリープを追加すればいい」スリープは競合状態を修正せず、隠すだけです。
「ユーザーはそんなことをしない」ユーザーは、あなたが想定しないことを何でもします。

クイックリファレンス

シナリオ解決策
更新前の残高確認アトミックな条件付き更新
カウンターのインクリメントアトミックなインクリメント(Redis INCR、SQL += 1)
複雑な複数ステップの操作ロックを使用したデータベーストランザクション
サービス間の操作分散ロック
リクエストの重複防止冪等性キー
バージョンの競合楽観的ロック

要点

正確性がタイミングに左右されるなら、それはバグです。

読み取り後の書き込みには競合が発生します。確認後の実行にも競合が発生します。状態を確認してから操作するまでの間には、どこであれ競合が発生します。アトミック操作、トランザクション、ロック、または冪等性キーを使用してください。「十分に速い」や「トラフィックが少ない」と決めつけてはいけません。

コメント