https://docs.spring.io/spring-batch/4.1.x/reference/html/retry.html#retry
https://qiita.com/kagamihoge/items/12fbbc2eac5b8a5ac1e0 俺の訳一覧リスト
1. Retry
処理をロバストかつ失敗の可能性を下げるには、次回以降には成功する確率が高い場合、その失敗した処理を自動リトライするのが有効な場合があります。断続的な障害に影響を受けやすいエラーは、実際のところ一時的な事が多いです。たとえば、ネットワークエラーやDB更新によるDeadlockLoserDataAccessException
が原因でwebサービスが失敗する、などです。
1.1. RetryTemplate
※ リトライ機能は2.2.0以降Spring Batchから分離しました。現在はSpring Retryの一部になっています。
自動リトライには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) { // webサービスなど、失敗する可能性のある何らかの処理 return result; } });
上の例では、webサービスを呼んで結果を返しています。もしwebサービスが失敗する場合、タイムアウトに達するまでリトライします。
1.1.1. RetryContext
RetryCallback
のメソッド引数はRetryContext
です。コールバックでこのコンテキストが必要になることはほとんどありませんが、イテレーション中のデータ格納場所として使えます。
同一スレッド実行中にネストしたリトライががあると、RetryContext
は親コンテキストを持ちます。親コンテキストはexecute
間でデータ共有したい場合に有用な場合があります。
1.1.2. RecoveryCallback
リトライが全滅すると、RetryOperations
はRecoveryCallback
という別のコールバックに制御を渡します。このコールバックを使うには、以下のように、これらのコールバックを一緒に渡します。
Foo foo = template.execute(new RetryCallback<Foo>() { public Foo doWithRetry(RetryContext context) { // ビジネスロジックをここに書く。 }, new RecoveryCallback<Foo>() { Foo recover(RetryContext context) throws Exception { // リカバリのロジックをここに書く。 } });
テンプレートがアボートするより前の段階でビジネスロジックが失敗する場合、リカバリコールバックで何らかの代替処理ができます。
1.1.3. Stateless Retry
極めて単純化すれば、リトライとはwhileループです。RetryTemplate
は成功するか失敗するまでトライし続けますRetryContext
にはリトライかアボートするかを決定するための情報を持ちますが、この状態はスタックにありグローバルに持つ必要がないので、これをステートレスリトライと呼びます。ステートレスかステートフルかの区別はRetryPolicy
の実装によります(RetryTemplate
はどちらも可能)。ステートレスでは、失敗したリトライと同一スレッドでリトライコールバックを実行します。
1.1.4. Stateful Retry
ある失敗がトランザクショナルなリソースをinvalidにする場合、特別な考慮が必要です。シンプルなリモートコールはトランザクショナルなリソースが(基本的には)無いのでこれに当てはまりませんが、Hibernateなどを使用するデータベース更新は該当します。この場合、即時失敗するように例外を再スローするのが唯一正解で、これによりトランザクションはロールバックして新しくvalidなトランザクションを開始します。
このようにトランザクションを含む場合、ステートレスリトライは不十分で、because the re-throw and roll back necessarily involve leaving the RetryOperations.execute() method and potentially losing the context that was on the stack. データロスを回避するには、スタックを何らかの保存機構に移してヒープに移動します。これのために、Spring BatchにはRetryContextCache
があり、RetryTemplate
に設定可能です。RetryContextCache
のデフォルトリスナはインメモリのシンプルなMap
です。クラスタ環境のマルチプロセッサの高度な使用法はある種のクラスタキャッシュでRetryContextCache
を実装すると思われます(ただし、クラスタ環境であっても、やりすぎな感があります)。
RetryOperations
のその他の責務は失敗したオペレーションが新規実行で復帰(通常は新規トランザクションでラップ)した事の識別です。これにはSpring BatchのRetryState
があります。RetryOperations
の特殊なexecute
メソッドと組み合わせて使います。
失敗したオペレーションの識別方法は、リトライ複数実行間で共有する状態により行います。状態の識別には、アイテム識別のユニークキーを返す責務のRetryState
を設定します。識別子はRetryContextCache
のキーとして使われます。
※ RetryState
が返すキーのObject.equals()
とObject.hashCode()
の実装には十分注意してください。アイテム識別用のビジネスロジック上のキーを使用してください。JMSメッセージの場合、メッセージIDを使用可能です。
リトライ全滅時、RetryCallback
の代わりに、別の方法で失敗アイテムを処理するオプションがあります (which is now presumed to be likely to fail)。ステートレスの場合同様、このオプションはRecoveryCallback
で行い、RetryOperations
のexecute
にこのコールバックを渡します。
リトライするかどうかの決定はRetryPolicy
にデリゲートするので、リミットやタイムアウトなど一般的な設定事項はここで行います(詳細はこのチャプター後半)。
1.2. Retry Policies
RetryTemplate
は内部的に、execute
メソッドのリトライか失敗かの決定はRetryPolicy
で行い、これはRetryContext
のファクトリでもあります。RetryTemplate
はRetryContext
の作成にその時点で設定されているポリシーを使用し、RetryCallback
の毎回の呼び出しに渡します。コールバックが失敗すると、RetryTemplate
はRetryPolicy
に(RetryContext
の)状態を更新するよう依頼し、次回のリトライをするかどうかをポリシーに問い合わせます。次回リトライしない場合(リミットやタイムアウトなど)、ポリシーはリトライ全滅状態をハンドリングする責務を持ちます。シンプルな実装はRetryExhaustedException
をスローし、エンクロージングのトランザクションはロールバックします。より複雑な実装でなんらかのリカバリアクションを取る事が可能で、トランザクションを残したままに出来ます。
※ 失敗は本来的にリトライ可能かそうでないかのどちらかです。ビジネスロジックが同一の例外を常にスローする場合、リトライするのは良くありません。またすべての例外型をリトライは禁止です。そうではなく、リトライ可能と人が判断可能な例外のみを対象にしてください。通常、ビジネスロジックで何でもかんでもリトライするのは有害ではないですが、ただの浪費です。失敗が決定論的な場合、致命的と分かっているものをリトライするのは時間の無駄です。
Spring BatchはステートレスRetryPolicy
のシンプルな汎用実装、SimpleRetryPolicy
とTimeoutRetryPolicy
、を提供します(例は前述)。
SimpleRetryPolicy
は例外リストに対して固定回数リトライします。また、リトライしたくない"fatal"例外も設定可能で、これはリトライ可能例外リストをオーバーライドします。これにより、リトライの振る舞いをより細かく制御します。以下はその例です。
SimpleRetryPolicy policy = new SimpleRetryPolicy(); // 最大リトライ回数 policy.setMaxAttempts(5); // すべての例外に対しリトライ(デフォルト) policy.setRetryableExceptions(new Class[] {Exception.class}); // ただし、IllegalStateExceptionはリトライしない policy.setFatalExceptions(new Class[] {IllegalStateException.class}); // 上記ポリシーを使用 RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(policy); template.execute(new RetryCallback<Foo>() { public Foo doWithRetry(RetryContext context) { // ここにビジネスロジック } });
より柔軟な実装ExceptionClassifierRetryPolicy
もあり、ExceptionClassifier
で例外セットに対して異なるリトライを設定できます。例外をRetryPolicy
にデリゲートするclassifierを呼ぶという動作にポリシーがなります。たとえば、ある例外については何回かリトライするように異なるポリシーでマッピングする、が出来ます。
場合によって、更にカスタマイズした判断ロジックでリトライポリシーを実装したいケースがあります。たとえば、あるカスタムリトライポリシーで、well-knownでソリューション固有の例外をリトライ可能かそうでないか、というものを作れます。
1.3. Backoff Policies
一時的なエラー後にリトライする場合、これらの問題はwait以外に解決策が無い場合が多いため、再試行前にwaitするのが有用です。RetryCallback
失敗時に、RetryTemplate
はBackoffPolicy
に従った実行の一時停止が出来ます。
以下コードはBackOffPolicy
のインタフェース定義です。
public interface BackoffPolicy { BackOffContext start(RetryContext context); void backOff(BackOffContext backOffContext) throws BackOffInterruptedException; }
BackoffPolicy
でバックオフをどう実装するかは自由です。Spring Batchが提供するポリシーはデフォルト設定ではすべてObject.wait()
を使います。バックオフのよくある使い方はwait間隔を指数関数的に増加させるもので、これは2つのリトライがロックしたり両方とも失敗するのを避けるためです(イーサネットから得た教訓)。これをするには、Spring BatchのExponentialBackoffPolicy
を使います。
1.4. Listeners
複数リトライの横断的関心事を扱うコールバックを追加すると便利な場合があります。これのために、Spring BatchはRetryListener
を提供しています。RetryTemplate
にはRetryListeners
を登録可能で、リスナーにはイテレーション中に発生するThrowable
とRetryContext
を渡します。
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
は逆順になります。
1.5. Declarative Retry
ビジネスロジック実行をリトライしたい場合があります。これの古典的なサンプルはリモートサービス呼び出しです。Spring BatchにはRetryOperations
でメソッド呼び出しをラップするAOPインターセプターの仕組みがあります。RetryOperationsInterceptor
はインターセプトメソッドを実行し、RetryTemplate
に指定した``RetryPolicy```に従って失敗時にリトライします。
以下は宣言的リトライのサンプルで、remoteCall
というサービスメソッドのリトライをjava configurationでしています(詳細はSpring User GuideのAOPインターセプター設定を参照してください)。
@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
を設定してください。