https://github.com/spring-projects/spring-retry 初めて使ったんで記念にREADME読んだ。
spring-retry - README.md
本プロジェクトはSpringアプリケーションで宣言的なリトライ機能を提供します。Spring Batch, Spring Integration, Spring for Apache Hadoopや、それ以外でも、使用できます。
Quick Start
例:
@Configuration @EnableRetry public class Application { @Bean public Service service() { return new Service(); } } @Service class Service { @Retryable(RemoteAccessException.class) public void service() { // ... do something } @Recover public void recover(RemoteAccessException e) { // ... panic } }
"service"メソッドを呼ぶ時にRemoteAccessException
で失敗する場合はリトライし(デフォルトは三回まで)、すべてのリトライが不成功に終わる場合は"recover"を呼びます。
@Retryable
アノテーションの属性には様々なオプションがあり、リトライする例外のinclude, exclude指定、リトライ回数制限、バックオフポリシー、などがあります。
Building
Requires Java 1.7 and Maven 3.0.5 (or greater)
$ mvn install
Features and API
RetryTemplate
処理をロバストにして障害に繋がりにくくするには、もし試行回数を増やせば成功する可能性があるなら、失敗した操作を自動リトライすると良い場合があります。そういう類のエラーは本質的には一時的です。たとえば、webサービスやRMIのリモート呼び出しの失敗は、ネットワーク障害やDBアップデートでのDeadLockLoserException
が原因になることがありますが、これらは短時間経過後に解消する場合があります。そうした操作のリトライを自動化するため、Spring RetryにはRetryOperations
があります。RetryOperations
インタフェースは以下の通りです。
public interface RetryOperations { <T> T execute(RetryCallback<T> retryCallback) throws Exception; <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback) throws Exception; <T> T execute(RetryCallback<T> retryCallback, RetryState retryState) throws Exception, ExhaustedRetryException; <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState) throws Exception; }
この基礎となるコールバックはシンプルなインターフェースで、ここにリトライするビジネスロジックを指定します。
public interface RetryCallback<T> { T doWithRetry(RetryContext context) throws Throwable; }
コールバックを実行して失敗(Exception
のスロー)すると、成功するか実装でアボートするまで、リトライします。
RetryOperations
インタフェースには各種のオーバーロードメソッドがあり、引数に、すべてのリトライが失敗する場合の様々なリカバリ、クライアントと実装で呼び出し中の情報を保存するリトライステータス(詳細後述)、を指定できます。
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サービスを実行してユーザに結果を返しています。もし呼び出しが失敗する場合はタイムアウトに達するまでリトライします。
RetryContext
RetryCallback
のメソッド引数はRetryContext
です。基本的にはコールバックでこのコンテキストを使うことは少ないですが、場合によってはリトライの繰り返し中にデータを保存する領域として使用します。
RetryContext
は、同一スレッド内で実行中のネストしたリトライがある場合、親コンテキストを持ちます。親コンテキストは呼び出し中に共有したいデータを格納するのに使用します。
RecoveryCallback
リトライがすべて失敗する場合にRetryOperations
は別のコールバックRecoveryCallback
に制御を渡せます。この機能を使うにはメソッドに一緒に2つのコールバックを渡します。
Foo foo = template.execute(new RetryCallback<Foo>() { public Foo doWithRetry(RetryContext context) { // ビジネスロジック }, new RecoveryCallback<Foo>() { Foo recover(RetryContext context) throws Exception { // リカバリロジック } });
テンプレートがアボートをする前にビジネスロジックが失敗する場合、リカバリコールバック経由で何らかの代替処理を行えます。
Stateless Retry
シンプルなケースではリトライは単にwhileループです。RetryTemplate
は成功か失敗するまで試行し続けます。RetryContext
はリトライかアボートするかを決定するための状態を持ちますが、この状態はスタック上にありグローバルに置く必要が無いので、これはステートレスリトライ(Stateless Retry)と呼びます。ステートレスとステートフルリトライの違いはRetryPolicy
の実装にあります(RetryTemplate
は両方とも出来ます)。ステートレスリトライでは、コールバックは失敗時のリトライと同一スレッドで実行します。
Stateful Retry
ある失敗がトランザクションリソースを無効にするケースの場合、何らかの考慮が必要です。これはシンプルなリモートコールでは無関係で、その理由は(基本的には)トランザクションリソースが無いためですが、Hibernateを使用しているなど、DB更新の場合には関係してきます。この場合には、失敗となった例外を即座に再スローすることだけが理にかなっており、これによって、トランザクションはロールバックして新しいトランザクションを開始します。
こうした場合ではステートレスリトライでは不十分で、再スローとロールバックはRetryOperations.execute()
を終わる必要がありスタックにあったコンテキストを潜在的にロストするためです。このロストを回避するにはスタックではなくヒープ(少なくとも)を使うストレージ機能が必要です。Spring RetryにはRetryContextCache
があり、これをRetryTemplate
に追加します。RetryContextCache
のデフォルト実装はインメモリでMap
を使います。メモリリーク回避のため、厳密に強制される最大容量は持ちますが、time to liveなど高度なキャッシュ機能はありません。必要に応じてそうした機能を持つMap
の使用を考慮してください。また、クラスタでの複数プロセッサによる高度な環境においても何らかのクラスタキャッシュ機能を持つRetryContextCache
の実装を検討してください(とはいえ、クラスタ環境だとしても、これはやり過ぎかもしれませんが)。
RetryOperations
の責務の一つは、新規実行(基本的には新規トランザクションでラップ)に復帰する際、失敗した操作を認識することです。これを扱うには、Spring RetryではRetryState
を使います。これはRetryOperations
の特別なexecuteメソッドと組み合わせて動作します。
失敗した操作を認識する方法とは、複数のリトライに横断する状態を識別することです。状態の識別には、アイテムを識別するユニークキーを返す責務を持つRetryState
オブジェクトをユーザが返します。識別子はRetryContextCache
でキーとして使われます。
Warning: RetryState
が返すキーのObject.equals()
とObject.hashCode()
は注意深く実装してください。アイテムの識別にはビジネスキーを使用するのが良いです。例としてJMSメッセージではメッセージIDを使用できます。
リトライが失敗する場合、(失敗する可能性が高い)RetryCallback
を呼ぶのではなく、失敗を処理する別の方法もあります。ステートレスの場合同様にRecoveryCallback
を使用する方法で、これはRetryOperations
のexecuteメソッドに渡します。
リトライするかどうかの決定は通常はRetryPolicy
にデリゲートするので、リミットとタイムアウトに関する関心事はここに入れます(後述)。
Retry Policies
RetryTemplate
の内部では、executeメソッドのリトライか失敗かの決定はRetryPolicy
が判断し、また、RetryContext
のファクトリでもあります。RetryTemplate
の責務には、RetryContext
を生成するポリシーを使うことと、毎回のリトライ時にRetryCallback
へRetryContext
を渡すこと、があります。コールバックが失敗すると、RetryTemplate
はRetryPolicy
に(RetryContext
内の)状態の更新を行うための呼び出しをする責務があり、それから、次回のリトライが実行可能かどうかをポリシーに問い合わせます。次回のリトライが実行不可能(例:リミットに達した、タイムアウトを超えた)な場合、ポリシーは失敗の状態を識別する責務はありますが、例外ハンドリングの背sキムはありません。RetryTemplate
はオリジナルの例外をスローし、ステートフルの場合以外でリカバリが利用可能では無い場合はRetryExhaustedException
をスローします。また、コールバック(ユーザのコードのこと)から無条件にオリジナルの例外をスローするようなフラグをRetryTemplate
に設定することも出来ます。
Tip: 失敗は本質的にはリトライ可能か不可能かのどちらかです。もしビジネスロジックがどんな場合でも同一の例外をスローする場合、リトライでその例外は使えません。かといってすべての例外型でリトライするのは止めて下さい。プログラマがリトライ可能と判断した例外でだけリトライしてください。ビジネスロジックを積極的にリトライするのは基本的には邪悪ではないですが、もし失敗が決定論的な場合、fatalが自明なリトライは時間を浪費するだけなので無駄です。
Spring Retryはシンプルで汎用のステートレスRetryPolicyの実装をいくつか提供しており、例えば上述の例で、SimpleRetryPolicy
とTimeoutRetryPolicy
を使っています。
SimpleRetryPolicy
は、指定の回数、例外型のリストに対してリトライします。
// 初回リトライを含む最大回数および // すべての例外(デフォルト)に対するリトライを設定 SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true)); // 上記のポリシーを適用 RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(policy); template.execute(new RetryCallback<Foo>() { public Foo doWithRetry(RetryContext context) { // ここにビジネスロジックを記述 } });
また、ExceptionClassifierRetryPolicy
というより柔軟な実装があり、これはExceptionClassifier
を使用して複数の例外型にそれぞれのリトライを設定できます。このポリシーはclassifierを呼び出して例外をRetryPolicyへのデリゲートに変換するので、たとえば、ある例外型を異なるポリシーにマッピングすることで、その例外型は他よりも多くリトライするようなことが可能です。
必要であれば自前のカスタムリトライポリシーを実装して下さい。たとえば、ソリューション固有で頻出なものがある場合、例外のclassificationをリトライ可能・不可能に割り当てます。
Backoff Policies
一時的な問題の発生後に再度リトライする場合、すこし待つのが良い場合が多く、基本的にはそうした問題の解決には待機だけが有効だからです。RetryCallback
が失敗するときに、RetryTemplate
は設定されたBackoffPolicy
に従って例外を待機させられます。
public interface BackoffPolicy { BackOffContext start(RetryContext context); void backOff(BackOffContext backOffContext) throws BackOffInterruptedException; }
BackoffPolicy
ではbackOffを任意の方法で実装します。Spring Retryが提供するすべてのポリシーのデフォルト動作はObject.wait()
を使います。backoffのよくある使い方は待機間隔を指数関数的に増加させるもので、これは二つのリトライがロックして両方とも失敗するのを避けるためです。これはイーサネットから得られた教訓です。Spring RetryはExponentialBackoffPolicy
を用意しています。また、ランダム版のディレイポリシーもあり、これは複雑なシステムで関連性のある失敗が共鳴しあうのを避けたい場合に有用です。
Listeners
場合によっては、複数の異なるリトライに対する横断的関心事のためのコールバックを追加すると便利なケースがあります。Spring RetryにはRetryListener
があります。RetryTemplate
でRetryListenersを登録可能で、そのリトライ中で利用可能なRetryContext
とThrowable
とコールバックがリスナに渡されます。
インタフェースは以下の通りです。
public interface RetryListener { void open(RetryContext context, RetryCallback<T> callback); void onError(RetryContext context, RetryCallback<T> callback, Throwable e); void close(RetryContext context, RetryCallback<T> callback, Throwable e); }
最もシンプルなケースのリトライではopenとcloseコールバックはリトライ全体の前後に呼ばれてonError
は毎回のRetryCallback
で呼ばれます。closeもThrowable
を受け取ります。何らかのエラーがある場合はRetryCallback
が最後に投げたものになります。
注意点として、複数のリスナはリストに格納されるので順序を持ちます。この場合、openはその順序で呼ばれますが、onErrorとcloseは逆順で呼ばれます。
Declarative Retry
ある種のビジネスロジックが呼ばれるときはすべて毎回リトライさせたい場合があります。これの良くある例はリモートサービス呼び出しです。Spring RetryはAOPのインターセプタを提供しており、これはメソッド呼び出しをRetryOperations
でラップします。RetryOperationsInterceptor
はインターセプトされたメソッドを実行し、設定されているRepeatTemplate
のRetryPolicy
に従って失敗時にリトライします。
Java Configuration for Retry Proxies
@Configuration
に@EnableRetry
を追加して、リトライしたいメソッド(全メソッドに対しては型レベル)に@Retryable
を付与します。また、複数のリトライリスナを指定できます。
@Configuration @EnableRetry public class Application { @Bean public Service service() { return new Service(); } @Bean public RetryListener retryListener1() { return new RetryListener() {...} } @Bean public RetryListener retryListener2() { return new RetryListener() {...} } } @Service class Service { @Retryable(RemoteAccessException.class) public service() { // ... do something } }
@Retryable
の属性でRetryPolicy
とBackoffPolicy
が制御できます。
@Service class Service { @Retryable(maxAttempts=12, backoff=@Backoff(delay=100, maxDelay=500)) public service() { // ... do something } }
100から500ミリ秒のランダムバックオフで12回まで試行します。また、stateful
属性(デフォルトfalse)でリトライをステートフルにするかどうかを制御します。ステートフルにするにはインターセプトされるメソッドに引数を持たせる必要があり、それが状態のキャッシュキー構築に使われます。
また、@EnableRetry
は、Sleeper
型のbean・RetryTemplate
内で使われるその他のストラテジーオブジェクト・実行時にリトライの振る舞いを制御するためのインターセプター、を参照します。
@EnableRetry
は@Retryable
beansのプロキシを生成し、そのプロキシ(アプリケーションのbeanインスタンス)にはRetryable
インタフェースを追加します。@EnableRetry
は単にマーカーインタフェースですが、ある種のツールがリトライアドバイスを適用したい場合に役に立ちます(こうしたツールはbeanがRetryable
を実装しているかどうかを基本的に考慮しない)。
リトライがすべて失敗する場合のエラー処理を設けたい場合はリカバリメソッドを作ります。@Retryable
とそれに対する@Recover
のメソッドは同一クラスに宣言します。リカバリの戻り値型は@Retryable
と一致させる必要があります。引数は任意でスローされる例外を含めることが可能で、また、同じく任意で大本のretryableのメソッドに渡される引数も含めることが可能です(引数の一部だけや引数無しも可能)。
@Service class Service { @Retryable(RemoteAccessException.class) public void service(String str1, String str2) { // 何かしらの処理 } @Recover public void recover(RemoteAccessException e, String str1, String str2) { // 必要に応じて大本の引数を使用してエラーハンドリング } }
Version 1.2で特定プロパティの式を使用する機能を導入しました。
@Retryable(exceptionExpression="#{message.contains('this can be retried')}") public void service1() { ... } @Retryable(exceptionExpression="#{message.contains('this can be retried')}") public void service2() { ... } @Retryable(exceptionExpression="#{@exceptionChecker.shouldRetry(#root)}", maxAttemptsExpression = "#{@integerFiveBean}", backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}")) public void service3() { ... }
いわゆるSpring SpEL式(#{...}
)を使用します。
式は#{${max.delay}}
もしくは#{@exceptionChecker.${retry.method}(#root)}
などのプロパティ・プレースホルダを持ちます。
exceptionExpression
はスローされる例外である#root
オブジェクトに対して評価が行われます。maxAttemptsExpression
と@BackOff
の式の属性は初期化中に一度だけ評価が行われます。こちらは評価用のルートオブジェクトはありませんが、コンテキスト内の他のbeanを参照できます。
XML Configuration
以下はSpring AOPを使用してremoteCall
というサービスメソッドをリトライする設定の例です(AOPインターセプターの設定の詳細についてはSpring User Guideを参照してください)。
<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"/>
上記例はインターセプター付属のデフォルトのRetryTemplate
を使用しています。ポリシーやリスナーを変更するには、インターセプターにRetryTemplate
のインスタンスをインジェクションする必要があります。
Contributing
Spring Retryはnon-restrictive Apache 2.0 licenseでリリースしており、一般的なGithub開発プロセスに従い、issueにはGithub trackerを使用してmasterにpull requestをマージしています。何か小さなことでもcontributeを望む場合遠慮は無用ですが、以下のガイドラインには従ってください。
non-trivialのパッチもしくはpull requestをアクセプトする前に、我々は https://cla.pivotal.io/*1へのサインを必要とします。contributor's agreementへのサインはメインリポジトリへのコミット権を意味しませんが、contributionsの受け入れ可能なことを意味し、無事通ればauthor creditを得られます。アクティブな開発者はコアチームへの参加を求められる場合があり、このときpull requestのマージ権が与えられます。
Code of Conduct
本プロジェクトにはContributor Covenantがあります。参加する場合、この規範を守ることを期待します。よからぬ行動に対しては spring-code-of-conduct@pivotal.io へ報告してください。
*1:リンクがあるが壊れている……