ステートマシンと格闘するのはやめよう:Temporalで構築するNode.jsの耐障害性ワークフロー

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

場当たり的なロジックから耐久性のある実行モデルへ

誰もが一度は経験したことがあるはずです。分散システムで長時間実行するプロセスを管理しようとすると、最初はシンプルなsetTimeoutや基本的なcronジョブから始まります。バックグラウンドタスクにBullMQを使うこともあるでしょう。しかし、30日間のユーザーオンボーディングシーケンスや高度決済パイプラインなど、ビジネスロジックが複雑化するにつれ、こうした自作ソリューションは崩壊し始めます。気づけば、本来の機能開発よりもリトライと状態永続化のための「接着剤コード」を書くことに時間を費やしていることになります。

従来のアプローチ vs 耐久性のある実行モデル

標準的なNode.js環境では、サーバーのクラッシュはメモリ上の状態にとって死を意味します。すべての進捗マーカーを手動でデータベースに保存していない限り、そのプロセスは消えてしまいます。StripeやTwilioなどの外部APIがダウンすれば、カスタムの指数バックオフロジックを書き、デッドレターキューを監視するだけで精一杯になります。

Temporalはこの問題を耐久性のある実行(Durable Execution)という概念で根本から覆します。状態をあなたが細かく管理する代わりに、Temporalがコードの各ステップを記録します。ワーカープロセスがクラッシュしても、Temporalは別のマシン上で処理を再開します。ローカル変数とスタックは、中断した時点の状態のまま完全に復元されます。バックエンド全体のアーキテクチャに「セーブポイント機能」が備わるようなイメージです。

機能 従来型(キュー+DB) Temporalのアプローチ
状態追跡 各ステップで手動UPDATEクエリ 自動かつ透過的
リトライ 脆弱なカスタムコードのループ 宣言的で堅牢なポリシー
タイムアウト 週単位の追跡が悪夢になる 数ヶ月単位のスリープをネイティブサポート
可視性 カスタム管理ダッシュボードを自作 完全な実行履歴付きのOOTB UI

Temporalを導入する際のリアルな話

Temporalは単なるライブラリではなく、コードの考え方そのものを根本から変えるものです。大きな頭痛の種を解決してくれる一方で、チームが習得すべき独自のルールセットも存在します。

気に入るであろう点

  • 盤石な信頼性:ワークフローは一時的な障害に対してほぼ無敵になります。ワーカーが落ちても、別のワーカーが1ミリ秒の進捗も失わずにバトンを引き継ぎます。
  • 直線的なコード:シンプルなスクリプトのように見えるコードを書けます。「10行目と11行目の間で電源が落ちたらどうなるか?」と心配する必要はもうありません。
  • タイムトラベル:テスト環境では時間をモックできます。30日間の請求サイクルを200ミリ秒未満で検証できます。

トレードオフ

  • インフラのオーバーヘッド:Temporalクラスターの管理が必要です。データベース(PostgresまたはCassandra)とElasticsearchのようなインデックスエンジンが必要になります。
  • 決定論のルール:これが最大のポイントです。ワークフローのコードは決定論的でなければなりません。Workflow内でMath.random()new Date()、直接のfetch()呼び出しは使えません。これらは「Activity」の中に置く必要があります。
  • 学習曲線:オーケストレーション(Workflow)と実行(Activity)の分離を理解するために、チームには1〜2週間の慣れが必要です。

本番環境に耐えうるアーキテクチャ

数千の同時ワークフローを処理するシステムには、疎結合な構造を推奨します。トリガーと実行を分離することで、高負荷下でもAPIの応答性を維持できます。

  1. Temporalクラスター:処理の中枢です。Dockerで自己ホストするか、Temporal Cloudにメンテナンスを任せることができます。
  2. ワーカー:専用のNode.jsプロセスです。HTTPリクエストは処理せず、Temporalサーバーにタスクをポーリングして実行するだけです。
  3. クライアント:既存のExpressやFastify APIです。その役割は「ユーザーXに対してこのワークフローを開始してください」とTemporalに伝えるだけです。

TypeScriptはここでは必須です。Node.js SDKは高度な型マッピングを使用して、クライアントとワーカーの同期を保ち、本番環境に到達する前にバグを検出します。

実践:サブスクリプションエンジンの構築

実際のシナリオを見てみましょう。顧客に$29.99を請求し、ウェルカムメールを送信する必要があります。ネットワークの不具合で支払いが失敗した場合はリトライし、5回失敗したら担当者にエスカレーションします。

ステップ1:Activityのコーディング

ActivityはシステムのワーカーでありAPI呼び出しやデータベースへの書き込みなど、現実世界の複雑な処理を担当します。

// activities.ts
export async function processPayment(amount: number): Promise<string> {
  // 実際のアプリではStripeのSDKを呼び出す
  if (Math.random() < 0.1) throw new Error("上流ゲートウェイタイムアウト");
  return "CHARGED_SUCCESSFULLY";
}

export async function sendWelcomeEmail(email: string): Promise<void> {
  console.log(`メールを送信しました: ${email}`);
}

ステップ2:ワークフローのオーケストレーション

ここが魔法の場所です。裏側で複雑なリトライロジックを処理しているにもかかわらず、コードがいかにクリーンで順次的に見えるかに注目してください。

// workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities';

const { processPayment, sendWelcomeEmail } = proxyActivities<typeof activities>({
  startToCloseTimeout: '1 minute',
  retry: {
    initialInterval: '2s',
    maximumAttempts: 5,
    backoffCoefficient: 2,
  },
});

export async function subscriptionWorkflow(email: string, amount: number): Promise<void> {
  const status = await processPayment(amount);
  
  if (status === 'CHARGED_SUCCESSFULLY') {
    await sendWelcomeEmail(email);
  }

  // このスリープは30日間続く可能性がある
  // ワーカーが100回再起動してもTemporalはこのタイマーを忘れない
  await sleep('30 days');
}

ステップ3:ワーカーの起動

ワーカーはクラスターに接続し、作業を待ちます。分散システムの機関室といえる存在です。

// worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';

async function run() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activities,
    taskQueue: 'billing-v1',
  });
  
  await worker.run();
}

run().catch((err) => { 
  console.error("ワーカーがクラッシュしました:", err);
  process.exit(1);
});

本番環境で得た苦労の教訓

複数の高トラフィックプロジェクトにTemporalをデプロイしてきた経験から、デバッグの時間を何時間も節約できる4つのヒントを紹介します。

1. バージョニングを尊重する

ワークフローは長命です。30日間のプロセスが実行中にworkflows.tsのコードを変更すると、履歴がコードと一致しなくなり「リプレイ」が失敗します。破壊的なロジック変更を安全に導入するには、必ずpatch APIを使用してください。

2. 冪等性はあなたの最大の味方

タスクが完了した瞬間にワーカーが落ちた場合、Activityが複数回実行される可能性があります。同じ操作で顧客に二重請求しないよう、StripeやデータベースへのリクエストにはIDempotencyキーを必ず渡してください。Redisを活用した冪等性の実装パターンも、この問題への対策として非常に有効です。

3. Web UIを活用する

TemporalのWeb UIはゲームチェンジャーです。すべてのイベントの視覚的なタイムラインを提供します。ワークフローがスタックしたとき、ログを掘り返すだけでなく、UIでどのActivityがタイムアウトしているかを確認し、失敗の完全なスタックトレースをレビューしましょう。

4. インタラクティブ性にはSignalを使う

ワークフローは「起動したら放置」ではありません。月の途中でユーザーが「サブスクリプションをキャンセル」をクリックするような外部イベントを処理するには、Signalを使いましょう。内部状態を失わずに、ワークフローが現実世界の出来事に反応できるようになります。

Temporalで構築するということは、「このエラーをどうキャッチするか?」という発想から「このプロセスはどう進化すべきか?」という発想への転換を意味します。状態管理の重い作業をオフロードすることで、実際にビジネスを動かすロジックに集中できるようになります。

Share: