kagamihogeの日記

kagamihogeの日記です。

Spring Batch 4.1.x - Reference Documentation - Retryのテキトー翻訳

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

リトライが全滅すると、RetryOperationsRecoveryCallbackという別のコールバックに制御を渡します。このコールバックを使うには、以下のように、これらのコールバックを一緒に渡します。

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で行い、RetryOperationsexecuteにこのコールバックを渡します。

リトライするかどうかの決定はRetryPolicyにデリゲートするので、リミットやタイムアウトなど一般的な設定事項はここで行います(詳細はこのチャプター後半)。

1.2. Retry Policies

RetryTemplateは内部的に、executeメソッドのリトライか失敗かの決定はRetryPolicyで行い、これはRetryContextのファクトリでもあります。RetryTemplateRetryContextの作成にその時点で設定されているポリシーを使用し、RetryCallbackの毎回の呼び出しに渡します。コールバックが失敗すると、RetryTemplateRetryPolicyに(RetryContextの)状態を更新するよう依頼し、次回のリトライをするかどうかをポリシーに問い合わせます。次回リトライしない場合(リミットやタイムアウトなど)、ポリシーはリトライ全滅状態をハンドリングする責務を持ちます。シンプルな実装はRetryExhaustedExceptionをスローし、エンクロージングのトランザクションロールバックします。より複雑な実装でなんらかのリカバリアクションを取る事が可能で、トランザクションを残したままに出来ます。

※ 失敗は本来的にリトライ可能かそうでないかのどちらかです。ビジネスロジックが同一の例外を常にスローする場合、リトライするのは良くありません。またすべての例外型をリトライは禁止です。そうではなく、リトライ可能と人が判断可能な例外のみを対象にしてください。通常、ビジネスロジックで何でもかんでもリトライするのは有害ではないですが、ただの浪費です。失敗が決定論的な場合、致命的と分かっているものをリトライするのは時間の無駄です。

Spring BatchはステートレスRetryPolicyのシンプルな汎用実装、SimpleRetryPolicyTimeoutRetryPolicy、を提供します(例は前述)。

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失敗時に、RetryTemplateBackoffPolicyに従った実行の一時停止が出来ます。

以下コードは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を登録可能で、リスナーにはイテレーション中に発生するThrowableRetryContextを渡します。

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);
}

opencloseは最もシンプルなケースではリトライ全体の前後に呼び出され、onErrorRetryCallbackのたびに呼ばれます。closeにはThrowableも渡されます。エラーがあった場合、RetryCallbackが最後にスローしたものが渡されます。

注意点として、複数リスナーがある場合、これはリストになり順序を持ちます。この場合、openはリスト順になりますがonErrorcloseは逆順になります。

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を設定してください。