Cron Jobという名の悪夢と、戦いの日々
悪夢は、1万人の顧客を抱える請求処理システムが、ある夜突然「悲鳴」を上げたことから始まりました。当初、私は午前1時に実行されるシンプルなCron Jobを使用していました。StripeのAPIに障害が発生するまでは、すべてが順調でした。結果は悲惨なものでした。5,000人の支払いが未完了のまま、残りの顧客には二重請求が発生してしまったのです。手動でジョブを再起動したものの、現在の正確な状態を把握できていなかったことが原因でした。
Cron JobやBullMQのようなキューライブラリの根本的な問題は、状態(ステート)の管理が非常に難しいことです。コードが途中でクラッシュした場合、実行途中のステップから正確に復旧させるのは至難の業です。自分でデータベースに状態を保存するロジックを書き、リトライをハンドリングし、データの整合性を気にかけなければなりません。
そこで出会ったのがTemporal.ioです。これは単なるスケジューリングツールではありません。これはDurable Execution(持続可能な実行)プラットフォームです。簡単に言えば、あなたのコードを最後まで完遂させるための仕組みです。サーバーがダウンしようが、ネットワークが切れようが、データベースがメンテナンス中であろうが、Temporalは粘り強くタスクを完了させます。
比較:Temporal vs 既存のソリューション
なぜ従来のソリューションが大規模なプロジェクトで限界を迎えるのか、現実を見てみましょう:
- Cron Job: 手軽ですが「投げっぱなし(fire and forget)」になりがちです。細かいタスクごとのリトライメカニズムがなく、何が起きているかを確認するダッシュボードもありません.
- Message Queues (BullMQ, RabbitMQ): Ack/Retryの仕組みがある分、マシです。しかし、5つのステップが絡み合うような複雑なロジックになると、すぐに「コールバック地獄」や複雑すぎるステートマシンの罠に陥ります。
- Temporal.io: まるでエラーが存在しないかのようにコードを書けます。Temporalは各行の実行ログを自動的に記録します。もし10行目でサーバーが死んでも、復活したときには最初からやり直すのではなく、11行目から再開されます。
なぜNode.jsプロジェクトにTemporalが必要なのか?
レガシーシステムをリファクタリングした際、Temporalによってエラー処理の冗長なコードを80%削減できることに気づきました。その理由は主に3つあります:
- 絶対的な信頼性: exponential backoff(指数バックオフ)による自動リトライ。一時的なエラーに対してエラーハンドリングのロジックを書く必要はありません。
- 可視性: ダッシュボードで、ワークフローが停止している時点の各変数の状態を詳細に確認できます。デバッグがこれまでにないほど楽になります。
- 長期的なタスク: sleep関数を使って、ワークフローを数ヶ月間待機させることができます。待機中、システムのリソース(RAMやCPU)を消費することはありません。
Node.jsでTemporalを導入する5つのステップ
ステップ1:Dockerで環境構築
最も素早く体験する方法は、Docker Composeを使用してTemporal ServerとUIのスタック全体を手元に用意することです:
curl -sL https://temporal.io/docker-compose.yml -o docker-compose.yml
docker-compose up
数分後、http://localhost:8080にアクセスしてください。ワークフローのコントロールセンターへようこそ。
ステップ2:SDKのインストール
ターミナルを開き、Node.jsプロジェクトに必要なパッケージを追加します:
npm install @temporalio/workflow @temporalio/activity @temporalio/client @temporalio/worker
ステップ3:Activity(実行タスク)の定義
Activityは、サードパーティAPIの呼び出しやデータベースクエリなど、リスクを伴うロジックを記述する場所です。
// activities.ts
export async function sendWelcomeEmail(email: string): Promise<string> {
// SendGridやMailchimpのAPI呼び出しをシミュレート
console.log(`${email} にメールを送信中...`);
return `Success: ${email}`;
}
ステップ4:Workflow(オーケストレーター)の構築
WorkflowはActivityを調整します。注意:ここでのコードはdeterministic(決定論的)でなければなりません。
// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { sendWelcomeEmail } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
retry: { maximumAttempts: 10 },
});
export async function welcomeWorkflow(email: string): Promise<string> {
return await sendWelcomeEmail(email);
}
ステップ5:Workerの起動
Workerはコードを実行するプロセスであり、Clientはワークフローをトリガーする場所です。
// 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-queue',
});
await worker.run();
}
run().catch(console.error);
教訓:決定論(Determinism)を壊してはいけない
初心者が陥りやすいミスは、Workflow内で直接 Math.random() や new Date() を使ってしまうことです。Temporalはイベントの「再生(replay)」によって動作します。再実行のたびに結果が変わると、システムは即座にエラーを報告します。
解決策: Date.now() の代わりに workflow.now() を使用してください。外部(API、DB)とのやり取りはすべて Activity 内で行う必要があります。Workflowから直接呼び出してはいけません。
6ヶ月使用しての正直な評価
得られた成果: デプロイやサーバーメンテナンスのたびに不安になることがなくなりました。時間をスキップしてワークフローをテストできる機能(time-skipping)のおかげで、テスト待ち時間を大幅に短縮できました。
注意点: Temporal Serverクラスターの運用コストがかかります。また、WorkflowとActivityを分離するという考え方に慣れるまでは、コードが少し冗長に感じられるかもしれません。
まとめ:いつ移行すべきか?
シンプルなバックグラウンドタスクが数個あるだけなら、Cron Jobのままでも良いでしょう。しかし、決済システム、予約システム、あるいは100%の正確性が求められるビジネスプロセスを構築しているのであれば、Temporalは最高の投資になります。エラー処理のコードを書く時間はもう終わりにしましょう。インフラはTemporalに任せ、あなたはビジネスロジックの開発に集中してください。

