mcpField
I
idempotency-handling

idempotency-handling

github.com/secondsky開発242409 回インストール5 週間前に更新
リポジトリ

idempotency-handling について

操作を繰り返しても結果が一貫するよう、冪等性の実装を支援するスキルです。キー方式とデータベース方式に加え、期限切れデータの整理方法も扱います。

主な機能

  1. 01リクエスト処理での冪等性キー
  2. 02リクエストとレスポンスのデータベース記録
  3. 03リクエスト本文ハッシュの検証
  4. 04有効期限とクリーンアップ戦略
  5. 05アトミック操作の指針

活用シーン

  1. 01決済の重複処理防止
  2. 02重複した注文作成リクエストへの対応
  3. 03Webhook やメール処理の重複排除

インストール

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

$npx skills add https://github.com/secondsky/claude-skills --skill idempotency-handling

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

詳細

冪等性の処理

実行回数にかかわらず、操作が同じ結果を生成するようにします。

冪等性キーのパターン

const redis = require('redis');
const client = redis.createClient();

async function idempotencyMiddleware(req, res, next) {
  const key = req.headers['idempotency-key'];
  if (!key) return next();

  const cached = await client.get(`idempotency:${key}`);
  if (cached) {
    const { status, body } = JSON.parse(cached);
    return res.status(status).json(body);
  }

  // Store original send
  const originalSend = res.json.bind(res);
  res.json = async (body) => {
    await client.setEx(
      `idempotency:${key}`,
      86400, // 24 hours
      JSON.stringify({ status: res.statusCode, body })
    );
    return originalSend(body);
  };

  next();
}

データベースを利用した冪等性

CREATE TABLE idempotency_keys (
  key VARCHAR(255) PRIMARY KEY,
  request_hash VARCHAR(64) NOT NULL,
  response JSONB,
  status VARCHAR(20) DEFAULT 'processing',
  created_at TIMESTAMP DEFAULT NOW(),
  expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '24 hours'
);

CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
async function processPayment(idempotencyKey, payload) {
  const requestHash = crypto.createHash('sha256')
    .update(JSON.stringify(payload)).digest('hex');

  // Try to insert with 'processing' status - only one request will succeed
  const insertResult = await db.query(
    `INSERT INTO idempotency_keys (key, request_hash, status)
     VALUES ($1, $2, 'processing')
     ON CONFLICT (key) DO NOTHING
     RETURNING *`,
    [idempotencyKey, requestHash]
  );

  // If we inserted the row (rowCount === 1), we're responsible for processing
  if (insertResult.rowCount === 1) {
    try {
      // Execute the payment
      const result = await executePayment(payload);

      // Update to completed with response
      await db.query(
        'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
        ['completed', JSON.stringify(result), idempotencyKey]
      );

      return result;
    } catch (error) {
      // Mark as failed on error
      await db.query(
        'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
        ['failed', JSON.stringify({ error: error.message }), idempotencyKey]
      );
      throw error;
    }
  }

  // Another request is/was processing this key - check status
  const existing = await db.query(
    'SELECT * FROM idempotency_keys WHERE key = $1',
    [idempotencyKey]
  );

  const row = existing.rows[0];
  if (!row) {
    throw new Error('Unexpected: idempotency key vanished');
  }

  // Verify request hasn't changed
  if (row.request_hash !== requestHash) {
    throw new Error('Idempotency key reused with different request');
  }

  // Check status
  if (row.status === 'completed') {
    return JSON.parse(row.response);
  } else if (row.status === 'processing') {
    throw new Error('Request already processing - retry later');
  } else if (row.status === 'failed') {
    const failedResponse = JSON.parse(row.response);
    throw new Error(`Previous attempt failed: ${failedResponse.error}`);
  }

  throw new Error(`Unknown status: ${row.status}`);
}

適用する場面

  • 決済処理
  • 注文の作成
  • Webhook の処理
  • メールの送信
  • 重複によって問題が発生するあらゆる操作

ベストプラクティス

  • 変更操作では冪等性キーを必須にします
  • リクエスト本文が保存済みのリクエストと一致することを検証します
  • 適切な TTL を設定します(通常は 24 時間)
  • アトミックなデータベース操作を使用します
  • テーブルの肥大化を防ぐため、クリーンアップジョブを実装します
TTL のクリーンアップ戦略

テーブルが際限なく増大するのを防ぐため、期限切れのキーを定期的にクリーンアップします。

オプション 1:スケジュールされたデータベースジョブ(PostgreSQL)

-- Run hourly via pg_cron or external scheduler
DELETE FROM idempotency_keys
WHERE expires_at < NOW()
LIMIT 1000; -- Batch delete to avoid long locks

オプション 2:アプリケーションのクリーンアップジョブ(Node.js)

// Run via cron or job scheduler (e.g., node-cron, Bull)
async function cleanupExpiredKeys() {
  try {
    const result = await db.query(
      'DELETE FROM idempotency_keys WHERE expires_at < NOW()'
    );
    console.log(`Cleaned up ${result.rowCount} expired idempotency keys`);
  } catch (error) {
    console.error('Cleanup job failed:', error);
  }
}

// Schedule to run every hour
cron.schedule('0 * * * *', cleanupExpiredKeys);

オプション 3:アプリケーションのクリーンアップジョブ(Python)

import asyncio
from datetime import datetime

async def cleanup_expired_keys():
    """Remove expired idempotency keys to prevent table bloat."""
    try:
        result = await db.execute(
            "DELETE FROM idempotency_keys WHERE expires_at < $1",
            datetime.now()
        )
        print(f"Cleaned up {result} expired idempotency keys")
    except Exception as e:
        print(f"Cleanup job failed: {e}")

# Run with APScheduler, Celery, or similar
# scheduler.add_job(cleanup_expired_keys, 'interval', hours=1)

クリーンアップのベストプラクティス:

  • ロック競合を最小限に抑えるため、トラフィックの少ない時間帯にクリーンアップを実行します
  • 大規模なテーブルでは、バッチ削除(LIMIT 1000)を使用します
  • クリーンアップジョブの実行状況と失敗を監視します
  • クリーンアップを容易にするため、created_at によるテーブルのパーティショニングを検討します
  • テーブルサイズが予期せず増大した場合に通知するアラートを設定します

コメント