概要

バッチ処理が外部APIやデータベースに依存している場合、ネットワークの瞬断やサーバーの一時的な過負荷によるエラーは避けられません。こうした一過性のエラーに対して即座に処理を打ち切るのではなく、適切な間隔を空けてリトライすることで成功率を高められます。ただし、固定間隔でのリトライはサーバーへの負荷集中を招くため、指数バックオフ(1秒→2秒→4秒→8秒と待機時間を倍増させる方式)が実務では標準的な戦略です。この記事では、Callable インターフェースを活用した汎用的な RetryExecutor クラスを実装し、リトライ対象の例外判定、最大リトライ回数の制御、待機時間の上限設定を扱います。

使いどころ

外部REST APIからデータを取得するバッチで、タイムアウトやHTTP 503に対してリトライする

DB接続プールが一時的に枯渇した場合に、一定間隔を空けて再接続を試みる

ファイル転送バッチでSFTPサーバーへの接続失敗時にバックオフ付きリトライする

コード例

RetryExecutor — 指数バックオフ付きリトライ
import java.util.concurrent.Callable;
import java.util.logging.Logger;

public class RetryExecutor {
    private static final Logger LOGGER = Logger.getLogger(RetryExecutor.class.getName());
    private final int maxRetries;
    private final long initialDelay;
    private final long maxDelay;

    public RetryExecutor(int maxRetries, long initialDelay, long maxDelay) {
        this.maxRetries = maxRetries;
        this.initialDelay = initialDelay;
        this.maxDelay = maxDelay;
    }

    public <T> T execute(Callable<T> task) throws Exception {
        int attempt = 0;
        long delay = initialDelay;
        while (true) {
            try {
                return task.call();
            } catch (Exception e) {
                attempt++;
                if (!isRetryable(e) || attempt >= maxRetries) {
                    LOGGER.severe("リトライ上限または対象外: " + e.getMessage());
                    throw e;
                }
                LOGGER.warning(String.format("リトライ %d/%d — %dms後 [%s]",
                    attempt, maxRetries, delay, e.getMessage()));
                try { Thread.sleep(delay); } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("割り込み発生", ie);
                }
                delay = Math.min(delay * 2, maxDelay);
            }
        }
    }

    private boolean isRetryable(Exception e) {
        return e instanceof java.io.IOException
            || e instanceof java.net.SocketTimeoutException;
    }

    public static void main(String[] args) throws Exception {
        RetryExecutor retry = new RetryExecutor(4, 1000L, 16000L);
        String result = retry.execute(new Callable<String>() {
            private int count = 0;
            @Override public String call() throws Exception {
                count++;
                if (count < 3) throw new java.io.IOException("接続失敗(試行" + count + ")");
                return "API応答: OK";
            }
        });
        System.out.println(result);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var で型推論が使える程度の違い。テキストブロックでログメッセージが読みやすくなる。

Java 17
// Java 17: 記述は同じだが var で簡潔に
var retryable = e instanceof java.net.SocketTimeoutException
    || e instanceof java.io.IOException;

Library Comparison

Pure Java(Callable + Thread.sleep)外部依存を増やしたくない場合。リトライ対象が限定的な場合。サーキットブレーカーが必要になると拡張コストが上がる。
Resilience4jリトライ・サーキットブレーカー・バルクヘッドを組み合わせたい場合。バッチ単体には過剰。依存と学習コストが増える。
Spring RetrySpring Boot で @Retryable による宣言的リトライを使いたい場合。Spring 依存が前提。Pure Java 構成には馴染まない。

注意点

リトライ対象の例外を限定すること。NullPointerException のようなプログラムエラーをリトライしても意味がない

最大リトライ回数と待機時間の上限を必ず設定する。無制限リトライはバッチの無限停滞を招く

Thread.sleep は InterruptedException を発生させるため、割り込みフラグの復元を忘れないこと

指数バックオフの初期値と倍率はシステム要件に合わせて調整する

リトライ回数とエラー内容はログに記録し、運用監視で検知できるようにする

FAQ

指数バックオフの初期待機時間の目安は。

外部APIなら1秒、DB再接続なら500ミリ秒が目安です。レートリミットがある場合はAPI仕様に従います。

リトライ中にバッチ全体のタイムアウトを超えた場合は。

RetryExecutor に制限時間チェックを組み込むか、外側のスケジューラでタイムアウト制御を行います。

リトライとサーキットブレーカーの違いは。

リトライは失敗を再実行、サーキットブレーカーは連続失敗でリクエスト自体を遮断します。組み合わせて使うことが多いです。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます