DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでTemporal.io耐久性ワークフローを設計する:長時間処理・Sagaパターン・補償

はじめに

マイクロサービスにおける長時間処理や分散トランザクションは、障害時の整合性確保が難しい。Temporal.ioは「1トランザクション=1ワークフロー」の考え方で、耐久性のある実行を保証する。Claude Codeを使い、Sagaパターンと補償処理を中心に設計手順を紹介する。

1トランザクション=1ワークフロー設計

Temporalの基本思想は、1ビジネストランザクションを1つのワークフローとして表現することだ。ワークフローはクラッシュしても再開でき、状態はTemporalサーバーが永続化する。

import { proxyActivities, sleep } from '@temporalio/workflow';
import type { OrderActivities } from './activities';

const { reserveInventory, chargePayment, shipOrder } = proxyActivities<OrderActivities>({
  startToCloseTimeout: '30 minutes',
  retry: {
    maximumAttempts: 3,
    initialInterval: '1s',
    backoffCoefficient: 2,
  },
});

export async function orderWorkflow(orderId: string): Promise<void> {
  // 1トランザクション = このワークフロー全体
  await reserveInventory(orderId);
  await chargePayment(orderId);
  await shipOrder(orderId);
}
Enter fullscreen mode Exit fullscreen mode

Claude Codeへの指示例:「orderWorkflowを定義して。各ステップはアクティビティとして分離し、タイムアウトとリトライポリシーを設定して」

Sagaクラスと補償配列(逆順実行)

障害発生時に実行済みステップを逆順で巻き戻すSagaパターンを実装する。compensations配列に補償関数を積み上げ、エラー時にreverse()して順番に実行する。

class Saga {
  private compensations: Array<() => Promise<void>> = [];

  addCompensation(fn: () => Promise<void>): void {
    this.compensations.push(fn);
  }

  async compensate(): Promise<void> {
    // 逆順で補償を実行(最後に実行したものから戻す)
    const reversed = [...this.compensations].reverse();
    for (const compensation of reversed) {
      try {
        await compensation();
      } catch (err) {
        // 補償失敗はログのみ(ベストエフォート)
        console.error('Compensation failed:', err);
      }
    }
  }
}

export async function orderWorkflowWithSaga(orderId: string): Promise<void> {
  const saga = new Saga();

  try {
    await reserveInventory(orderId);
    saga.addCompensation(() => releaseInventory(orderId));

    await chargePayment(orderId);
    saga.addCompensation(() => refundPayment(orderId));

    await shipOrder(orderId);
    // 出荷後の補償はビジネスルール次第
    saga.addCompensation(() => cancelShipment(orderId));

  } catch (err) {
    await saga.compensate();
    throw ApplicationFailure.nonRetryable(
      `Order ${orderId} failed and compensated`,
      'ORDER_COMPENSATION_COMPLETE',
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

冪等なアクティビティ設計(存在チェック)

Temporalはアクティビティを自動リトライする。そのため各アクティビティは冪等でなければならない。「既に処理済みか」を存在チェックで判定し、二重実行を防ぐ。

export const activities: OrderActivities = {
  async reserveInventory(orderId: string): Promise<void> {
    // 存在チェックで冪等性を保証
    const existing = await db.query(
      'SELECT id FROM inventory_reservations WHERE order_id = $1',
      [orderId]
    );
    if (existing.rows.length > 0) {
      // 既に予約済み → 正常終了(冪等)
      return;
    }

    await db.query(
      'INSERT INTO inventory_reservations (order_id, created_at) VALUES ($1, NOW())',
      [orderId]
    );
  },

  async chargePayment(orderId: string): Promise<void> {
    const existing = await db.query(
      'SELECT id FROM payment_charges WHERE order_id = $1',
      [orderId]
    );
    if (existing.rows.length > 0) {
      return; // 二重請求防止
    }

    await paymentGateway.charge(orderId);
    await db.query(
      'INSERT INTO payment_charges (order_id, charged_at) VALUES ($1, NOW())',
      [orderId]
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

Claude Codeへの指示例:「reserveInventoryを冪等にして。order_idで既存レコードをチェックし、あれば早期リターンする実装にして」

ApplicationFailure.nonRetryableで補償完了を明示

Saga補償が完了した後は、これ以上リトライしないことをTemporalに伝える必要がある。ApplicationFailure.nonRetryableを使うことで、ワークフローを確実に失敗終了させる。

import { ApplicationFailure } from '@temporalio/workflow';

// 補償完了後 → nonRetryableで確実に失敗終了
throw ApplicationFailure.nonRetryable(
  `Order ${orderId} saga compensated successfully`,
  'SAGA_COMPENSATED',
  { orderId, compensatedAt: new Date().toISOString() }
);

// ワーカー側でエラー種別を判別可能
if (err instanceof ApplicationFailure && err.type === 'SAGA_COMPENSATED') {
  await notifyOrderFailed(orderId);
}
Enter fullscreen mode Exit fullscreen mode

リトライ可能なエラーとリトライ不可エラーを明確に分けることで、無限リトライループを防ぐ。

まとめ

  • 1トランザクション=1ワークフロー:Temporalが状態を永続化するため、クラッシュしても再開できる
  • Sagaパターンcompensations配列を逆順実行することで、分散トランザクションの巻き戻しを実現する
  • 冪等なアクティビティ:存在チェックで二重実行を防ぎ、安全なリトライを保証する
  • ApplicationFailure.nonRetryable:補償完了後に明示的な失敗終了を指示し、無限ループを回避する

Claude Codeにアーキテクチャ方針を伝えれば、Saga実装・冪等チェック・エラー種別分岐まで一気に生成できる。


みょうがのCode Review Pack(¥980)では、Temporal・マイクロサービス設計レビュー用のClaude Codeカスタムスキルを提供しています。
Code Review Pack を見る →

Top comments (0)