再試行
処理をより堅牢で失敗しにくくするために、失敗した操作が後続の試行で成功する可能性がある場合に自動的に再試行すると役立つ場合があります。断続的な障害の影響を受けやすいエラーは、通常一時的なものです。例には、ネットワーク障害またはデータベース更新の DeadlockLoserDataAccessException
が原因で失敗する Web サービスへのリモート呼び出しが含まれます。
RetryTemplate
再試行機能は、2.2.0 の時点で Spring Batch から引き出されました。現在、新しいライブラリ Spring Retry [GitHub] (英語) の一部です。 |
再試行操作を自動化するために、Spring Batch には RetryOperations
戦略があります。RetryOperations
の次のインターフェース定義:
public interface RetryOperations {
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState)
throws E, ExhaustedRetryException;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback,
RetryState retryState) throws E;
}
基本的なコールバックは、次のインターフェース定義に示すように、再試行するビジネスロジックを挿入できるシンプルなインターフェースです。
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
コールバックが実行され、失敗した場合(Exception
をスローすることにより)、成功するか実装が中止されるまで再試行されます。RetryOperations
インターフェースには、オーバーロードされた execute
メソッドがいくつかあります。これらのメソッドは、すべての再試行が終了したときの回復のさまざまなユースケースを処理し、再試行状態を処理します。これにより、クライアントと実装は呼び出し間で情報を保存できます(これについてはこの章の後半で詳しく説明します)。
RetryOperations
の最も単純な汎用実装は RetryTemplate
です。次のように使用できます。
RetryTemplate template = new RetryTemplate();
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);
template.setRetryPolicy(policy);
Foo result = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// Do stuff that might fail, e.g. webservice operation
return result;
}
});
前の例では、Web サービスの呼び出しを行い、結果をユーザーに返します。その呼び出しが失敗すると、タイムアウトに達するまで再試行されます。
RetryContext
RetryCallback
のメソッドパラメーターは RetryContext
です。多くのコールバックはコンテキストを無視しますが、必要に応じて、属性バッグとして使用して、反復中にデータを保存できます。
同じスレッドでネストされた再試行が進行中の場合、RetryContext
には親コンテキストがあります。親コンテキストは、execute
の呼び出し間で共有する必要があるデータを保存するのに役立つことがあります。
RecoveryCallback
再試行が使い果たされると、RetryOperations
は RecoveryCallback
と呼ばれる別のコールバックに制御を渡すことができます。この機能を使用するには、次の例に示すように、クライアントは同じメソッドにコールバックを一緒に渡します。
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
},
new RecoveryCallback<Foo>() {
Foo recover(RetryContext context) throws Exception {
// recover logic here
}
});
テンプレートが中止を決定する前にビジネスロジックが成功しなかった場合、クライアントには回復コールバックを介して代替処理を行う機会が与えられます。
ステートレス再試行
最も単純な場合、再試行は while ループにすぎません。RetryTemplate
は、成功または失敗するまで試行を続けることができます。RetryContext
には、再試行するか中止するかを決定する状態が含まれていますが、この状態はスタック上にあり、グローバルにどこにでも保存する必要がないため、このステートレス再試行と呼びます。ステートレス再試行とステートフル再試行の区別は、RetryPolicy
の実装に含まれています(RetryTemplate
は両方を処理できます)。ステートレス再試行では、再試行コールバックは常に、失敗したときと同じスレッドで実行されます。
ステートフルリトライ
障害によりトランザクションリソースが無効になる場合、いくつかの特別な考慮事項があります。トランザクションリソースがないため(通常)、これは単純なリモート呼び出しには適用されませんが、特に Hibernate を使用する場合は、データベースの更新に適用されることがあります。この場合、トランザクションをロールバックし、新しい有効なトランザクションを開始できるように、障害を呼び出した例外をすぐに再スローするだけです。
トランザクションが関係する場合、ステートレス再試行では十分ではありません。再スローとロールバックには必ず RetryOperations.execute()
メソッドを終了し、スタック上のコンテキストを失う可能性があるためです。失われないようにするには、スタックからスタックを持ち上げて(少なくとも)ヒープストレージに格納するストレージ戦略を導入する必要があります。このために、Spring Batch は RetryContextCache
と呼ばれるストレージ戦略を提供します。これは RetryTemplate
に注入できます。RetryContextCache
のデフォルトの実装はメモリ内にあり、単純な Map
を使用しています。クラスター環境で複数のプロセスを使用する高度な使用箇所では、RetryContextCache
を何らかのクラスターキャッシュで実装することを検討することもできます(ただし、クラスター環境でも、これは過剰な場合があります)。
RetryOperations
の責任の一部は、失敗した操作が新しい実行で戻ってきたとき(および通常は新しいトランザクションでラップされたとき)に認識することです。これを容易にするために、Spring Batch は RetryState
抽象化を提供します。これは、RetryOperations
インターフェースの特別な execute
メソッドと連携して機能します。
失敗した操作を認識する方法は、再試行の複数の呼び出しにわたって状態を識別することです。状態を識別するために、ユーザーは、アイテムを識別する一意のキーを返す責任がある RetryState
オブジェクトを提供できます。この識別子は、RetryContextCache
インターフェースのキーとして使用されます。
|
再試行が使い果たされると、RetryCallback
を呼び出す代わりに、失敗したアイテムを別の方法で処理するオプションもあります(現在は失敗する可能性が高いと推定されています)。ステートレスの場合と同様に、このオプションは RecoveryCallback
によって提供されます。RecoveryCallback
は、RetryOperations
の execute
メソッドに渡すことで提供できます。
再試行するかどうかの決定は、実際には通常の RetryPolicy
に委譲されるため、制限とタイムアウトに関する通常の関心事をそこに注入できます(この章で後述します)。
再試行ポリシー
RetryTemplate
内では、execute
メソッドで再試行するか失敗するかの決定は、RetryContext
のファクトリでもある RetryPolicy
によって決定されます。RetryTemplate
には、現在のポリシーを使用して RetryContext
を作成し、試行ごとに RetryCallback
に渡す責任があります。コールバックが失敗した後、RetryTemplate
は RetryPolicy
を呼び出してその状態(RetryContext
に保存されている)を更新するように要求し、その後、別の試行が可能かどうかをポリシーに要求する必要があります。別の試行ができない場合(制限に達したときやタイムアウトが検出されたときなど)、ポリシーは枯渇状態の処理も担当します。単純な実装は RetryExhaustedException
をスローします。これにより、含まれているトランザクションがロールバックされます。より高度な実装では、何らかの回復アクションを実行しようとする場合があります。その場合、トランザクションはそのままの状態を保つことができます。
障害は本質的に再試行可能かどうかのどちらかです。同じ例外が常にビジネスロジックからスローされる場合は、再試行しても意味がありません。そのため、すべての例外型で再試行しないでください。むしろ、再試行できると予想される例外のみに焦点を合わせてください。通常、より積極的に再試行することはビジネスロジックに有害ではありませんが、障害が確定的である場合、事前に致命的であることがわかっているものの再試行に時間を費やすため、無駄です。 |
Spring Batch は、SimpleRetryPolicy
や TimeoutRetryPolicy
などのステートレス RetryPolicy
のいくつかの単純な汎用実装を提供します(前の例で使用)。
SimpleRetryPolicy
は、例外型の名前付きリストのいずれかで、一定回数まで再試行できます。また、再試行すべきではない「致命的な」例外のリストもあり、このリストは再試行可能なリストをオーバーライドするため、次の例に示すように、再試行動作をより細かく制御するために使用できます。
SimpleRetryPolicy policy = new SimpleRetryPolicy();
// Set the max retry attempts
policy.setMaxAttempts(5);
// Retry on all exceptions (this is the default)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ... but never retry IllegalStateException
policy.setFatalExceptions(new Class[] {IllegalStateException.class});
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
}
});
ExceptionClassifierRetryPolicy
と呼ばれるより柔軟な実装もあります。これにより、ユーザーは ExceptionClassifier
の抽象化を通じて、任意の例外型のセットに対して異なる再試行動作を構成できます。ポリシーは、分類子を呼び出して例外をデリゲート RetryPolicy
に変換することで機能します。例: ある例外型は、別のポリシーにマッピングすることで、失敗する前に別の型よりも何度も再試行できます。
ユーザーは、よりカスタマイズされた決定のために、独自の再試行ポリシーを実装する必要がある場合があります。たとえば、既知のソリューション固有の例外の再試行可能と再試行不可への分類がある場合、カスタム再試行ポリシーは意味があります。
バックオフポリシー
一時的な障害の後に再試行する場合、通常は再試行する前に少し待つことが役立ちます。通常、障害は待機することによってのみ解決できる問題によって引き起こされるためです。RetryCallback
が失敗した場合、RetryTemplate
は BackoffPolicy
に従って実行を一時停止できます。
次のコードは、BackOffPolicy
インターフェースのインターフェース定義を示しています。
public interface BackoffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException;
}
BackoffPolicy
は、任意の方法で backOff を自由に実装できます。Spring Batch によって提供されるポリシーは、すべて Object.wait()
を使用します。一般的な使用例は、ロック期間に入る 2 回の再試行と両方の失敗を回避するために、指数関数的に増加する待機期間でバックオフすることです(これはイーサネットから学んだ教訓です)。このために、Spring Batch は ExponentialBackoffPolicy
を提供します。
リスナー
多くの場合、複数の異なる再試行での横断的関心事のために追加のコールバックを受信できると便利です。このために、Spring Batch は RetryListener
インターフェースを提供します。RetryTemplate
を使用すると、ユーザーは RetryListeners
を登録でき、反復中に利用可能な場合は RetryContext
および Throwable
でコールバックが提供されます。
次のコードは、RetryListener
のインターフェース定義を示しています。
public interface RetryListener {
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}
open
および close
コールバックは、最も単純なケースでは再試行全体の前後に来ます。onError
は個々の RetryCallback
呼び出しに適用されます。close
メソッドも Throwable
を受け取る場合があります。エラーが発生した場合、RetryCallback
によって最後にスローされたものです。
複数のリスナーがある場合、リストにあるため、順序があることに注意してください。この場合、open
は同じ順序で呼び出され、onError
と close
は逆の順序で呼び出されます。
宣言的再試行
時々、それが起こるたびに再試行したいことがわかっているいくつかのビジネス処理があります。この典型的な例は、リモートサービスコールです。Spring Batch は、この目的のために RetryOperations
実装でメソッド呼び出しをラップする AOP インターセプターを提供します。RetryOperationsInterceptor
は、インターセプトされたメソッドを実行し、提供された RepeatTemplate
の RetryPolicy
に従って失敗時に再試行します。
次の例は、Spring AOP 名前空間を使用して remoteCall
というメソッドへのサービス呼び出しを再試行する宣言的再試行を示しています(AOP インターセプターの構成方法の詳細については、Spring ユーザーガイドを参照してください)。
<aop:config>
<aop:pointcut id="transactional"
expression="execution(* com..*Service.remoteCall(..))" />
<aop:advisor pointcut-ref="transactional"
advice-ref="retryAdvice" order="-1"/>
</aop:config>
<bean id="retryAdvice"
class="org.springframework.retry.interceptor.RetryOperationsInterceptor"/>
次の例は、java 構成を使用して remoteCall
というメソッドへのサービス呼び出しを再試行する宣言的再試行を示しています(AOP インターセプターの構成方法の詳細については、Spring ユーザーガイドを参照してください)。
@Bean
public MyService myService() {
ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
factory.setInterfaces(MyService.class);
factory.setTarget(new MyService());
MyService service = (MyService) factory.getProxy();
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns(".*remoteCall.*");
RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));
return service;
}
上記の例では、インターセプター内でデフォルトの RetryTemplate
を使用しています。ポリシーまたはリスナーを変更するには、RetryTemplate
のインスタンスをインターセプターに挿入できます。