定期実行タスクの『前回完了確認』機構がないまま本番化する──重複実行検知の実装難度と現場の判断ズレ

「一度だけ実行されたはず」という前提の危うさ

定期実行タスク、いわゆるバッチ処理やスケジュール実行のジョブが、予期せず二重三重に走ってしまう。これは多くの開発現場で経験する問題ですが、意外と事後的に発見されることが多いです。

問題は、開発段階では「実行予定時刻に一度だけ走る」という前提で実装が進むことです。けれど本番環境では、アプリケーションサーバーの再起動、スケジューラーの再起動、ネットワーク遅延による実行判定の曖昧性、複数サーバーでのスケジューラー稼働など、単純ではない状況が生まれます。

「前回の実行が本当に完了したのか」を確認する仕組みがないと、同じ処理が重複実行され、データの二重計上、売上の多重請求、ポイント加算の重複といった深刻な問題に直結します。

なぜ『前回完了確認』の実装が後回しになるのか

この機構が本番化前に組み込まれない理由は、単純な怠慢ではなく、実装難度と優先度の判断ズレにあります。

実装の複雑さ

前回の実行が「本当に完了した」ことを保証するには、以下の情報を管理する必要があります。

  • 最後に実行が開始された日時
  • 実行が正常終了したかどうか
  • 終了時刻(部分的な成功か完全な成功か)
  • 実行中に失敗した場合の状態

これらをデータベースに記録し、次回実行時に参照して「前回から十分な時間が経過し、かつ前回が完了している」ことを確認する。さらに、その確認処理自体が並行実行されないようロックを掛ける必要があります。

優先度の判断ズレ

開発段階では「重複実行なんて滅多に起きない」と見積もられやすいです。特に以下のような判断が現場では起きやすいです。

  • 本番環境でも開発環境と同じ頻度でしか実行されないだろう
  • サーバーが落ちることは稀だから、再起動による重複は考えなくていい
  • 複数サーバー構成になるのは、もっと後の話
  • 今は単一サーバーだから、OS レベルのスケジューラー(cron など)で十分

これらの判断は、その時点では間違っていません。ただし、本番運用が長くなり、サーバー構成が変わり、負荷が増えると、前提が崩れます。

『前回完了確認』の実装パターンと現実的な選択

一般的な実装パターンを、複雑さと信頼度の観点から整理します。

パターン1: 実行ログテーブルの単純な確認

-- ジョブ実行ログ
CREATE TABLE job_execution_log (
  id BIGINT PRIMARY KEY,
  job_name VARCHAR(255) NOT NULL,
  started_at TIMESTAMP NOT NULL,
  completed_at TIMESTAMP,
  status VARCHAR(50) NOT NULL, -- 'running', 'success', 'failure'
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

実行開始前に、最新のレコードを確認。completed_at が NULL なら前回は未完了と判断し、実行を中止または待機。

メリット: シンプルで理解しやすい
デメリット: 複数サーバーから同時にアクセスされると、ロック戦略がないと重複実行を完全には防げない

パターン2: 排他ロックを用いた実装

BEGIN TRANSACTION;
SELECT * FROM job_execution_log 
  WHERE job_name = ? 
  FOR UPDATE NOWAIT;
-- ロック取得に成功したら、前回の完了確認と新規レコード挿入
COMMIT;

トランザクション内でロックを取得し、その間に前回完了確認と新規レコード挿入を行う。他のプロセスはロック待ちになり、重複実行を防げます。

メリット: 強い一貫性を保証できる
デメリット: ロック競合によるパフォーマンス低下、デッドロックの可能性、分散トランザクションでは複雑化

パターン3: 分散ロック(Redis など)

SET job:lock:${job_name} ${server_id} NX EX 3600

Redis の SET ... NX EX を使用。ロック取得に成功したサーバーのみ実行。タイムアウト値を設定し、ハングアップ時の自動解放を仕組む。

メリット: スケーラブル、複数サーバー環境で有効
デメリット: Redis の可用性に依存、ネットワーク遅延による判定ズレの可能性

現場で起きる『完了確認なし本番化』のシナリオ

実際の運用では、以下のようなケースで問題が顕在化します。

シナリオA: 本番サーバーの再起動

デプロイやメンテナンス時にアプリケーションを再起動。スケジューラーが起動時に即座にジョブを実行してしまい、前回の実行がまだ進行中だった場合、重複実行が発生。

シナリオB: クラウドの自動スケーリング

負荷増加時に新しいインスタンスが起動され、各インスタンスで同じスケジューラー設定が走る。複数サーバーでの重複実行を検知する仕組みがないと、同時実行される。

シナリオC: バッチ処理の長時間化

当初は数分で終わるジョブだったが、データが増えるにつれ実行時間が延びる。スケジューラーの実行間隔は変わらないため、前回の実行が終わる前に次の実行が始まる。

実装判断のポイント

導入判断は、以下の観点で現実的に進めるべきです。

必須と判断すべきケース

  • 金銭に関わるデータを扱う(請求、決済、ポイント加算)
  • マスターデータの同期処理(顧客情報、商品情報の更新)
  • 複数サーバー構成またはその予定がある
  • バッチ処理の実行時間が不安定

後回しにできるケース

  • ログ出力やキャッシュ更新など、重複実行の影響が軽微
  • 実行時間が短く、スケジューラー間隔も十分に長い
  • 単一サーバー環境で、今後の拡張予定がない

実装コストとのバランス

小規模なチームの場合、完璧な分散ロック機構を最初から組むより、シンプルな実行ログ確認 + 定期的な監視(アラート)の組み合わせから始めるのは現実的です。その後、問題が顕在化したら、ロック機構に段階的に強化する。

運用段階での対応

実装と同じくらい重要なのが、運用時の検知と対応です。

  • 実行ログの定期確認: 同じジョブが短時間に複数回実行されていないか
  • データの一貫性チェック: 日次で重複データがないか(特に金銭関連)
  • アラート設定: 実行時間の異常延長、エラーの頻発を検知

完璧な防止機構がなくても、早期発見と手動修正で被害を最小化できます。

まとめ

定期実行タスクの重複実行防止は、実装難度が低くない割に、本番化前に後回しにされやすい要件です。その理由は、開発段階では「滅多に起きない」と見積もられるためです。

ただし本番運用が進むと、サーバー構成の変化、処理時間の変動、再起動の頻度など、前提が変わります。

現実的には、金銭や重要データを扱うなら最初から組み込む軽微な処理なら運用監視で対応するという判断が、チームの規模と案件の性質に応じて必要です。完璧さを目指すより、リスク度に応じた段階的な実装が、実務では有効です。