概要
バッチ処理が外部APIやデータベースに依存している場合、ネットワークの瞬断やサーバーの一時的な過負荷によるエラーは避けられません。こうした一過性のエラーに対して即座に処理を打ち切るのではなく、適切な間隔を空けてリトライすることで成功率を高められます。ただし、固定間隔でのリトライはサーバーへの負荷集中を招くため、指数バックオフ(1秒→2秒→4秒→8秒と待機時間を倍増させる方式)が実務では標準的な戦略です。この記事では、Callable インターフェースを活用した汎用的な RetryExecutor クラスを実装し、リトライ対象の例外判定、最大リトライ回数の制御、待機時間の上限設定を扱います。
使いどころ
外部REST APIからデータを取得するバッチで、タイムアウトやHTTP 503に対してリトライする
DB接続プールが一時的に枯渇した場合に、一定間隔を空けて再接続を試みる
ファイル転送バッチでSFTPサーバーへの接続失敗時にバックオフ付きリトライする
コード例
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);
}
}Version Coverage
var で型推論が使える程度の違い。テキストブロックでログメッセージが読みやすくなる。
// Java 17: 記述は同じだが var で簡潔に
var retryable = e instanceof java.net.SocketTimeoutException
|| e instanceof java.io.IOException;Library Comparison
注意点
リトライ対象の例外を限定すること。NullPointerException のようなプログラムエラーをリトライしても意味がない
最大リトライ回数と待機時間の上限を必ず設定する。無制限リトライはバッチの無限停滞を招く
Thread.sleep は InterruptedException を発生させるため、割り込みフラグの復元を忘れないこと
指数バックオフの初期値と倍率はシステム要件に合わせて調整する
リトライ回数とエラー内容はログに記録し、運用監視で検知できるようにする
FAQ
外部APIなら1秒、DB再接続なら500ミリ秒が目安です。レートリミットがある場合はAPI仕様に従います。
RetryExecutor に制限時間チェックを組み込むか、外側のスケジューラでタイムアウト制御を行います。
リトライは失敗を再実行、サーキットブレーカーは連続失敗でリクエスト自体を遮断します。組み合わせて使うことが多いです。