再試行

処理をより堅牢で失敗しにくくするために、失敗した操作が後続の試行で成功する可能性がある場合に自動的に再試行すると役立つ場合があります。断続的な障害の影響を受けやすいエラーは、通常一時的なものです。例には、ネットワーク障害またはデータベース更新の 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 インターフェースのキーとして使用されます。

RetryState によって返されるキーの Object.equals() および Object.hashCode() の実装には十分注意してください。最善のアドバイスは、ビジネスキーを使用してアイテムを識別することです。JMS メッセージの場合、メッセージ ID を使用できます。

再試行が使い果たされると、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 のインスタンスをインターセプターに挿入できます。