バッチ処理の失敗時リトライ戦略──冪等性の実装難易度と現場での『とりあえず動かす』の圧力

冪等性が求められる理由は明確だが、実装はそうではない

バッチ処理でリトライを安全に行うには、冪等性(同じ操作を何度実行しても結果が変わらない性質)が必須だというのは、もはや教科書的な知識です。しかし現場では、この理想と現実のギャップが頻繁に問題になります。

データベースの更新、外部APIへのリクエスト、ファイル生成といった一連の処理で障害が発生したとき、単純に「最初から実行し直す」では済まないケースが大多数です。部分的に成功した状態をどう扱うのか、冪等性を確保するために何を実装するのか──その判断が、プロジェクトの納期や運用コストを大きく左右します。

実務では、この問題に対して「とりあえず動かす」という圧力がかかります。設計段階で完全な冪等性を実現するコストと、本番運用で手作業対応するコストのバランスを、限られた情報の中で判断しなければならないのです。

冪等性の実装パターンと現場での選択肢

冪等性を確保する方法は、一つではありません。それぞれに前提条件と代償があります。

1. 処理済みフラグによる制御

最も単純な方法は、各バッチ実行単位に一意のIDを付与し、処理済みかどうかをデータベースに記録することです。

CREATE TABLE batch_execution_log (
  id BIGINT PRIMARY KEY,
  batch_name VARCHAR(100),
  execution_id VARCHAR(50) UNIQUE,
  status VARCHAR(20),
  started_at TIMESTAMP,
  completed_at TIMESTAMP
);

バッチ開始時に execution_id を生成し、各データ更新時に同じ execution_id を記録する。リトライ時に同じ execution_id が存在すれば、その部分はスキップするという仕組みです。

この方法の利点は単純さです。どのバッチ処理にも比較的容易に組み込めます。ただし、部分的な失敗時の状態管理が複雑になりやすく、「このバッチは途中まで成功したから、失敗した部分だけ再実行する」という判断を、誰がどのタイミングで行うのかが曖昧になりがちです。

2. 業務ロジック側での冪等性確保

より堅牢なアプローチは、業務ロジック自体を冪等に設計することです。たとえば、在庫数の加減ではなく「在庫を〇〇に設定する」という絶対値での更新にするといった工夫です。

# 悪い例:相対値で更新
def process_inventory_batch
  items.each do |item|
    item.stock += item.quantity_to_add
    item.save!
  end
end

# 良い例:絶対値で更新
def process_inventory_batch
  items.each do |item|
    item.stock = calculate_expected_stock(item)
    item.save!
  end
end

この方法なら、何度実行しても同じ結果になります。ただし、すべての処理がこのパターンに適応できるわけではありません。特に外部APIとの連携や、複数システム間のデータ同期では、相対値的な操作が避けられないことが多いです。

3. 外部APIのリトライ対応

決済処理や配送手配など、外部サービスを呼び出すバッチでは、APIの仕様に左右されます。多くの場合、APIプロバイダー側がリクエストIDによる重複排除を提供しており、同じIDで複数回リクエストすれば冪等に扱われます。

ただし、すべてのAPIがこの機能を持つわけではなく、タイムアウト後に実際に処理されたのかどうかを確認する手段が限定的なケースもあります。そういう場合は、バッチ側で「このリクエストは既に送信済みか」を追跡し、確認APIで状態を検証してから再送するといった工夫が必要になります。

現場で起きる『完全性』と『実現性』の衝突

理想的には、すべてのバッチ処理を完全に冪等に設計すべきです。しかし、現実には時間と人員の制約があります。

新規プロジェクトでは、バッチ処理の設計段階で冪等性をアーキテクチャに組み込む余裕があることもあります。しかし、既存システムへの機能追加では、既に動いているバッチに対して「リトライ時の冪等性を確保するため、この部分を書き直す」という判断は、なかなか優先順位が上がりません。

その結果、本番で障害が起きたときに、以下のような対応が現実的になります。

  • 手作業での状態確認と部分再実行:どこまで成功したのかを手動で確認し、失敗した部分だけを別プロセスで実行する
  • 一時的なスキップロジック:本番環境で、特定のレコードをスキップするフラグを立てて対応する
  • 翌日以降のリカバリー:その日のバッチは諦めて、翌日以降で整合性を取る

どれも理想的ではありませんが、運用チームの負担や追加開発の時間を考えると、現実的な選択肢になってしまうのです。

中小規模チームが取るべき現実的な判断軸

完全な冪等性設計は理想ですが、すべてのバッチに適用するのは現実的ではありません。以下の観点で優先順位を付けることをお勧めします。

優先度が高い処理

  • 金銭や在庫に直結する処理
  • 外部システムとの連携処理
  • 夜間に無人で実行される処理(障害発見が遅れるため)

優先度が低い処理

  • 単なるレポート生成やキャッシュ更新
  • 手作業で確認・修正しやすい処理
  • 失敗時の影響範囲が限定的な処理

高優先度の処理には、設計段階で冪等性を組み込むコストを惜しまない。低優先度の処理には、「リトライ時は手作業確認」という運用ルールを明文化しておく。このメリハリが、現実的な品質と開発効率のバランスを取る上で重要です。

また、バッチ処理の監視・アラート体制も同時に整備すれば、障害検知から対応までの時間が短縮でき、手作業対応の負担も減ります。

実装時の小さな工夫が運用を楽にする

完全な冪等性設計ができなくても、以下のような小さな工夫で、運用時の判断と対応を楽にできます。

  • 実行ログに処理単位の成否を記録する:バッチ全体の成否ではなく、各レコード単位での成否を記録しておけば、どこまで成功したのかが一目瞭然です
  • リトライ時の処理範囲を明確にする:「このバッチは前回の実行から〇時間経過していたら、〇行目から再開する」というルールを実装に組み込んでおく
  • 運用マニュアルに具体的な対応フローを記す:「このバッチが失敗したら、以下の手順で確認し、この場合は手動で〇〇を実行する」といった手順書を、実装と同時に作成する

これらは、完全な冪等性設計ほどの負担ではなく、本番運用での問題対応を大幅に効率化します。

おわりに

バッチ処理のリトライ戦略は、「冪等性が理想」という理論と「現場の制約」の間で、常にバランスを取る必要があります。その判断は、プロジェクトの規模、既存システムの状態、運用チームのスキルセットによって異なります。

完全性を追求して納期を逃すのも、実装を急いで本番運用を苦しめるのも、どちらも組織にとって損失です。「この処理には何が必要か」を冷徹に判断し、現実的な落としどころを見つけることが、実務では最も大切だと考えます。