レジリエンス機能

7.0 の時点で、コア Spring Framework には共通の回復力機能、特にメソッド呼び出し用の @Retryable および @ConcurrencyLimit アノテーションとプログラムによる再試行のサポートが含まれています。

@Retryable

@Retryable (Javadoc) は、個々のメソッド (メソッドレベルで宣言されたアノテーションを使用)、または特定のクラス階層内のすべてのプロキシ呼び出しメソッド (型レベルで宣言されたアノテーションを使用) の再試行特性を指定するアノテーションです。

@Retryable
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

デフォルトでは、例外が発生した場合、メソッド呼び出しは再試行されます。最初の失敗後、最大 3 回(maxRetries = 3)の再試行が行われ、再試行の間隔は 1 秒です。すべての再試行が失敗し、再試行ポリシーが尽きた場合、対象メソッドからの最後の元の例外が呼び出し元に伝播されます。

@Retryable メソッドは少なくとも 1 回呼び出され、最大 maxRetries 回再試行されます。ここで、maxRetries は再試行の最大回数です。具体的には、total attempts = 1 initial attempt + maxRetries attempts です。

例: maxRetries が 4 に設定されている場合、@Retryable メソッドは少なくとも 1 回、最大 5 回呼び出されます。

必要に応じて、各メソッドごとにカスタマイズできます。たとえば、includes 属性と excludes 属性を使用して、再試行する例外を絞り込むことができます。指定された例外型は、失敗した呼び出しによってスローされた例外だけでなく、ネストされた原因とも照合されます。

@Retryable(MessageDeliveryException.class)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}
@Retryable(MessageDeliveryException.class) は @Retryable(includes = MessageDeliveryException.class) のショートカットです。

高度な使用例では、@Retryable の predicate 属性を介してカスタム MethodRetryPredicate を指定できます。述語は、Method と指定された Throwable に基づいて、たとえば Throwable のメッセージをチェックすることによって、失敗したメソッド呼び出しを再試行するかどうかを決定するために使用されます。

カスタム述語は includes および excludes と組み合わせることができますが、カスタム述語は常に includes および excludes が適用された後に適用されます。

または、4 回の再試行と、若干のジッターを伴う指数バックオフ戦略の場合:

@Retryable(
        includes = MessageDeliveryException.class,
        maxRetries = 4,
        delay = 100,
        jitter = 10,
        multiplier = 2,
        maxDelay = 1000)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

最後になりましたが、@Retryable はリアクティブ戻り型を持つリアクティブメソッドでも機能し、パイプラインを Reactor の再試行機能で装飾します。

@Retryable(maxRetries = 4, delay = 100)
public Mono<Void> sendNotification() {
    return Mono.from(...); (1)
}
1 この生の Mono は再試行仕様で装飾されます。

さまざまな特性の詳細については、@Retryable (Javadoc) で使用可能なアノテーション属性を参照してください。

@Retryable のいくつかの属性には、上記の例で使用されている特別に型指定されたアノテーション属性の代替として、プロパティプレースホルダーと SpEL サポートを提供する String バリアントがあります。

@Retryable 処理中、Spring は対象メソッドから発生した例外ごとに MethodRetryEvent を発行します。これにより、すべての元の例外を追跡 / ログに記録できますが、@Retryable メソッドの呼び出し元は最後の例外しか見ることができません。

@ConcurrencyLimit

@ConcurrencyLimit (Javadoc) は、個々のメソッド (メソッドレベルで宣言されたアノテーションを使用)、または特定のクラス階層内のすべてのプロキシ呼び出しメソッド (型レベルで宣言されたアノテーションを使用) の同時実行制限を指定するアノテーションです。

@ConcurrencyLimit(10)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

これは、制限に達した場合にアクセスをブロックするスレッドプールまたは接続プールのプールサイズ制限の効果と同様に、同時に多数のスレッドからターゲットリソースへのアクセスが防止されることを目的としています。

オプションで制限を 1 に設定して、ターゲットの Bean インスタンスへのアクセスを効果的にロックすることもできます。

@ConcurrencyLimit(1)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

このような制限は、一般的にスレッドプールの制限がない仮想スレッドで特に有用です。非同期タスクの場合、これは SimpleAsyncTaskExecutor (Javadoc) で制約できます。同期呼び出しの場合、このアノテーションは ConcurrencyThrottleInterceptor (Javadoc) を通じて同等の動作を提供します。ConcurrencyThrottleInterceptor (Javadoc) は、Spring Framework 1.0 以降、AOP フレームワークを用いたプログラム的な使用のために提供されています。

@ConcurrencyLimit には、上記の int ベースの例の代替として、プロパティプレースホルダーと SpEL サポートを提供する limitString 属性もあります。

回復力のある方法の実現

Spring のコアとなるアノテーションベースの機能の多くと同様に、@Retryable と @ConcurrencyLimit はメタデータとして設計されており、適用するか無視するかを選択できます。レジリエンスアノテーションの処理を有効にする最も便利な方法は、対応する @Configuration クラスで @EnableResilientMethods (Javadoc) を宣言することです。

あるいは、コンテキストで RetryAnnotationBeanPostProcessor または ConcurrencyLimitBeanPostProcessor Bean を定義することで、これらのアノテーションを個別に有効にすることもできます。

プログラムによる再試行のサポート

ApplicationContext に登録された Bean 内のメソッドの再試行セマンティクスを指定するための宣言的なアプローチを提供する @Retryable とは対照的に、RetryTemplate (Javadoc) は任意のコードブロックを再試行するためのプログラム API を提供します。

具体的には、RetryTemplate は構成された RetryPolicy (Javadoc) に基づいて Retryable (Javadoc) 操作を実行し、場合によっては再試行します。

var retryTemplate = new RetryTemplate(); (1)

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1 暗黙的に RetryPolicy.withDefaults() を使用します。

デフォルトでは、再試行可能な操作は、スローされたすべての例外に対して再試行されます。最初の失敗後、最大 3 回の再試行 (maxRetries = 3) が行われ、試行間の遅延は 1 秒になります。

再試行可能な操作は、少なくとも 1 回実行され、最大 maxRetries 回再試行されます。ここで、maxRetries は再試行の最大回数です。具体的には、total attempts = 1 initial attempt + maxRetries attempts です。

例: maxRetries が 4 に設定されている場合、再試行可能な操作は少なくとも 1 回、最大 5 回呼び出されます。

再試行回数のみをカスタマイズする必要がある場合は、以下に示すように RetryPolicy.withMaxRetries() ファクトリメソッドを使用できます。

var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); (1)

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1RetryPolicy.withMaxRetries(4) を明示的に使用します。

再試行する例外の種類を絞り込む必要がある場合は、includes() および excludes() ビルダーメソッドを使用できます。指定された例外の種類は、失敗した操作によってスローされた例外や、ネストされた原因と照合されます。

var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class) (1)
        .excludes(...) (2)
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1 含める例外の種類を 1 つ以上指定します。
2 除外する例外型を 1 つ以上指定します。

高度な使用例では、RetryPolicy.Builder の predicate() メソッドを介してカスタム Predicate<Throwable> を指定できます。また、述語は、たとえば Throwable のメッセージをチェックすることによって、特定の Throwable に基づいて失敗した操作を再試行するかどうかを決定するために使用されます。

カスタム述語は includes および excludes と組み合わせることができますが、カスタム述語は常に includes および excludes が適用された後に適用されます。

次の例は、4 回の再試行と、少しのジッターを伴う指数バックオフ戦略を使用して RetryPolicy を構成する方法を示しています。

var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class)
        .maxRetries(4)
        .delay(Duration.ofMillis(100))
        .jitter(Duration.ofMillis(10))
        .multiplier(2)
        .maxDelay(Duration.ofSeconds(1))
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));

RetryPolicy のファクトリメソッドとビルダー API は、一般的な構成シナリオのほとんどをカバーしていますが、カスタム RetryPolicy を実装することで、再試行をトリガーする例外の種類と使用する BackOff (Javadoc) 戦略を完全に制御できます。また、RetryPolicy.Builder の backOff() メソッドを介して、カスタマイズされた BackOff 戦略を構成することもできます。

上記の例では、@Retryable メソッド呼び出しと同様のパターンが適用され、最後の元の例外が呼び出し元に伝播されます。これは、戻り値の有無にかかわらず利用可能な RetryTemplate の invoke バリアントを使用して行われます。コールバックはチェックされない例外をスローする可能性があり、その最後の例外は呼び出し元側で直接処理できるように公開されます。

try {
    retryTemplate.invoke(
            () -> jmsClient.destination("notifications").send(...));
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}
try {
    var result = retryTemplate.invoke(() -> {
        jmsClient.destination("notifications").send(...);
        return "result";
    });
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}

RetryTemplate インスタンスは非常に軽量で、必要に応じてオンザフライで作成でき、特定の呼び出しに対して使用する再試行ポリシーを指定することも可能です。

try {
    new RetryTemplate(RetryPolicy.withMaxRetries(4)).invoke(
            () -> jmsClient.destination("notifications").send(...));
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}

より詳細なやり取りを行うには、RetryTemplate の execute メソッドを使用できます。呼び出し元は、RetryTemplate によってスローされるチェック例外 RetryException を処理し、すべての試行の結果を公開する必要があります。

try {
    var result = retryTemplate.execute(() -> {
        jmsClient.destination("notifications").send(...);
        return "result";
    });
}
catch (RetryException ex) {
    // ex.getExceptions() / ex.getLastException() ...
}

RetryListener (Javadoc) は RetryTemplate に登録することで、重要な再試行ステップ (再試行の前後など) や、単にすべての呼び出し試行に対応し、コールバックから発生するすべての例外とすべての再試行結果 (枯渇、中断、タイムアウト) を追跡できます。これは、invoke を使用する場合に特に便利です。invoke では、最後の元の例外以外の再試行状態は公開されないためです。

var retryTemplate = new RetryTemplate();
retryTemplate.setRetryListener(new RetryListener() {
    @Override
    public void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
        ...
    }
});

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));

CompositeRetryListener (Javadoc) を介して複数のリスナーを構成することもできます。