Javaにおけるリトライパターン:適応的なリトライによるフォールトトレラントシステムの構築
別名
- リトライロジック
- リトライメカニズム
リトライデザインパターンの目的
Javaのリトライパターンは、特にネットワーク経由の外部リソースとの通信を含む特定の操作を透過的にリトライし、呼び出し側のコードをリトライ実装の詳細から分離します。これは、一時的な障害を適切に処理するレジリエントなソフトウェアシステムを開発するために不可欠です。
現実世界の例を用いたリトライパターンの詳細な説明
現実世界の例
宅配ドライバーが顧客の家へ荷物を配達しようとしていると想像してみてください。ドアベルを鳴らしますが、誰も応答しません。すぐに諦める代わりに、数分待ってからもう一度試み、このプロセスを数回繰り返します。これは、ソフトウェアにおけるリトライパターンと似ており、システムは(一時的なネットワークの不具合など)の問題が解決され、操作が成功することを期待して、失敗した操作(例:ネットワークリクエスト)を一定回数リトライしてから最終的に諦めます。
簡単に言うと
リトライパターンは、ネットワーク上の失敗した操作を透過的にリトライします。
Microsoftのドキュメントによると
サービスまたはネットワークリソースへの接続を試行する際に発生する一時的な障害を、失敗した操作を透過的にリトライすることでアプリケーションが処理できるようにします。これにより、アプリケーションの安定性が向上します。
Javaにおけるリトライパターンのプログラミング例
リトライデザインパターンは、アプリケーションが成功することを期待して、操作を複数回透過的に実行することを可能にするレジリエンスパターンです。このパターンは、一時的な障害が頻繁に発生するネットワークサービスまたはリモートリソースにアプリケーションが接続する場合に特に役立ちます。
まず、実行可能で`BusinessException`をスローする可能性のある操作を表す`BusinessOperation`インターフェースがあります。
public interface BusinessOperation<T> {
T perform() throws BusinessException;
}
次に、このインターフェースを実装する`FindCustomer`クラスがあります。このクラスは、最終的に顧客IDを返す前に、`BusinessException`を断続的にスローすることで、不安定なサービスをシミュレートします。
public final class FindCustomer implements BusinessOperation<String> {
@Override
public String perform() throws BusinessException {
// ...
}
}
`Retry`クラスは、リトライパターンが実装されている場所です。`BusinessOperation`と試行回数を受け取り、操作が成功するか、最大試行回数に達するまで、操作の実行を継続します。
public final class Retry<T> implements BusinessOperation<T> {
private final BusinessOperation<T> op;
private final int maxAttempts;
private final long delay;
private final AtomicInteger attempts;
private final Predicate<Exception> test;
private final List<Exception> errors;
@SafeVarargs
public Retry(
BusinessOperation<T> op,
int maxAttempts,
long delay,
Predicate<Exception>... ignoreTests
) {
this.op = op;
this.maxAttempts = maxAttempts;
this.delay = delay;
this.attempts = new AtomicInteger();
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
this.errors = new ArrayList<>();
}
public List<Exception> errors() {
return Collections.unmodifiableList(this.errors);
}
public int attempts() {
return this.attempts.intValue();
}
@Override
public T perform() throws BusinessException {
do {
try {
return this.op.perform();
} catch (BusinessException e) {
this.errors.add(e);
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
throw e;
}
try {
Thread.sleep(this.delay);
} catch (InterruptedException f) {
//ignore
}
}
} while (true);
}
}
このクラスでは、`perform`メソッドは操作の実行を試みます。操作が例外をスローした場合、例外が回復可能かどうか、最大試行回数に達していないかどうかを確認します。両方の条件が真の場合、指定された遅延時間待ってから再試行します。例外が回復不可能であるか、最大試行回数に達した場合は、例外を再スローします。
最後に、リトライパターンの例を駆動する`App`クラスを示します。
public final class App {
private static final Logger LOG = LoggerFactory.getLogger(App.class);
public static final String NOT_FOUND = "not found";
private static BusinessOperation<String> op;
public static void main(String[] args) throws Exception {
noErrors();
errorNoRetry();
errorWithRetry();
errorWithRetryExponentialBackoff();
}
private static void noErrors() throws Exception {
op = new FindCustomer("123");
op.perform();
LOG.info("Sometimes the operation executes with no errors.");
}
private static void errorNoRetry() throws Exception {
op = new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND));
try {
op.perform();
} catch (CustomerNotFoundException e) {
LOG.info("Yet the operation will throw an error every once in a while.");
}
}
private static void errorWithRetry() throws Exception {
final var retry = new Retry<>(
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
3, //3 attempts
100, //100 ms delay between attempts
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
);
op = retry;
final var customerId = op.perform();
LOG.info(String.format(
"However, retrying the operation while ignoring a recoverable error will eventually yield "
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
));
}
private static void errorWithRetryExponentialBackoff() throws Exception {
final var retry = new RetryExponentialBackoff<>(
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
6, //6 attempts
30000, //30 s max delay between attempts
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
);
op = retry;
final var customerId = op.perform();
LOG.info(String.format(
"However, retrying the operation while ignoring a recoverable error will eventually yield "
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
));
}
}
コードを実行すると、次のコンソール出力が生成されます。
10:12:19.573 [main] INFO com.iluwatar.retry.App -- Sometimes the operation executes with no errors.
10:12:19.575 [main] INFO com.iluwatar.retry.App -- Yet the operation will throw an error every once in a while.
10:12:19.682 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1
10:12:22.297 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1
このように、リトライパターンにより、アプリケーションは一時的な障害を適切に処理できるようになり、レジリエンスと信頼性が向上します。
Javaでリトライパターンを使用する場面
リトライパターンの適用は、特に以下の場合に効果的です。
- ネットワーク呼び出し、データベース接続、外部サービスとの統合など、操作が一時的に失敗する可能性がある場合。
- 一時的な障害が発生する可能性が高いが、リトライのコストが低いシナリオ。
Javaにおけるリトライパターンの現実世界の適用例
- 一時的な障害を処理するためのネットワーク通信ライブラリ。
- 一時的な停止またはタイムアウトを管理するためのデータベース接続ライブラリ。
- 一時的に利用できない可能性のあるサードパーティサービスと対話するAPI。
リトライパターンのメリットとトレードオフ
メリット
- アプリケーションの堅牢性とフォールトトレランスを向上させます。
- 一時的な障害の影響を大幅に軽減できます。
トレードオフ
- リトライのためにレイテンシが発生する可能性があります。
- 適切に管理されない場合、リソースの枯渇につながる可能性があります。
- 問題を悪化させないよう、リトライパラメータを慎重に設定する必要があります。
関連するJavaデザインパターン
- サーキットブレーカー:失敗のしきい値に達した後に外部サービスへのリクエストの流れを停止し、システムの過負荷を防ぐために使用されます。