Microprofile Fault Toleranceの機能と独自に用意したInterceptor間の実行順序を制御する

## 環境

* Java 8
* CDI 2.0
* Microprofile Fault Tolerance 2.0

## 事象

[Microprofile Fault Tolerance 2.0 Circuit Breaker](https://download.eclipse.org/microprofile/microprofile-fault-tolerance-2.0/microprofile-fault-tolerance-spec.html#circuitbreaker)のアノテーションと、独自に用意した `FooInterceptor` を実行するアノテーションを併用した場合、サーキットブレーカが意図せず発動する事象に遭遇しました。

```java:FooInterceptor
@Interceptor
@Dependent
@Foo
@Priority(Interceptor.Priority.APPLICATION)
public class FooInterceptor {
  
  @AroundInvoke
  public Object invoke(InvocationContext ic) throws Exception {
    try {
      return ic.proceed();

    } catch (SomeException e) {
      throw someCondition
        ? new WrappedException(e)
        : e;
    }
  }
}
```

```java:FooInterceptorを呼び出すためのアノテーション
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Foo {}
```

```java:サーキットブレーカとFooInterceptorを利用するクラス
@ApplicationScoped
public class SomeClass {

/**
 *  何かをする
 * @throws SomeException エラーが発生した
 * @throws WrappedException someConditionがtrueのときにエラーが発生した
 * @throws CircuitBreakerOpenException サーキットブレーカ発動中
 */
  @CircuitBreaker(
      successThreshold = 1,
      requestVolumeThreshold = 100,
      failureRatio = 1,
      delay = 600_000,
      failOn = {
        SomeException.class,
      })
  @Foo
  public void doSomeThing() {
    // 何かをする
  }
}
```
この実装では、someConditionがtrueの場合にはサーキットブレーカが発動しないことを期待していました。
`SomeClass#doSomeThing` にてthrowされた例外がまず `FooInterceptor` により　`WrappedException` に変換される。`WrappedException` は`failOn` にて定義していないため、サーキットブレーカは発動しないという想定です。しかし実際には、 `SomeClass#doSomeThing` が一定回数 `WrappedException` を返したところサーキットブレーカが発動し、その後は `CircuitBreakerOpenException` を返すようになりました。

## 解決策

以下の通り、FooInterceptorの優先順位を `5000` とすることで、someConditionがtrueの場合のサーキットブレーカ発動を回避することができました。

```java:独自に用意したInterceptor
@Interceptor
@Dependent
@Foo
// @Priority(Interceptor.Priority.APPLICATION)
@Priority(5000)
public class FooInterceptor {
  // 省略
}
```

## 原因

本事象は、`@CircuitBreaker` が呼び出すInterceptorのPriorityよりも、 `FooInterceptor` のPriorityが高かったために発生しました。  
実行されるべきInterceptorが複数存在するときの実行順序は、それらが持つ[Priorityにより決まります](https://javaee.github.io/javaee-spec/javadocs/javax/interceptor/Interceptor.Priority.html)。 `FooInterceptor` のPriorityがより高い場合には、まず `FooInterceptor#invoke` 、 次に `@CircuitBreakerが呼び出すInterceptor#invoke` 、最後に `SomeClass#doSomething` という順序でコールスタックが積まれていくことになります。このとき、 `SomeClass#doSomething` で発生した例外は `@CircuitBreakerが呼び出すInterceptor#invoke` が最初に受け取ることになるため、 `FooInterceptor#invoke` で実施している変換処理が呼ばれる前にサーキットブレーカ発動条件の評価が行われてしまいます。  

これを避けるためには、 `FooInterceptor` のPriorityを、 `@CircuitBreaker` が呼び出すInterceptorのそれよりも低く設定する必要があります。そうすることで、コールスタックの順序が入れ替わり、`FooInterceptor#invoke` が `SomeClass#doSomething` で発生した例外を先にcatchすることができるようになります。では、 `@CircuitBreaker` のPriorityはいくつに設定されているのでしょうか？

[Microprofile Fault Tolerance 2.0の仕様](https://download.eclipse.org/microprofile/microprofile-fault-tolerance-2.0/microprofile-fault-tolerance-spec.html#fault-tolerance-interceptor)では、以下のように記述されており、Microprofile Fault Toleranceの各Interceptorは `Priority.PLATFORM_AFTER+10` (4010)から `Priority.PLATFORM_AFTER+50` (4050)までの範囲にPriorityを定義するよう定めています。

> The base priority of the lowest priority Fault Tolerance interceptor is Priority.PLATFORM_AFTER+10, which is 4010. If more than one Fault Tolerance interceptor is provided by an implementation, the priority number taken by Fault Tolerance interceptor(s) should be in the range of [base, base+40].

`FooInterceptor` はこれよりもPriorityを低くする、つまりPriorityの値を大きくする必要があります。もともと設定していた `Priority.APPLICATION` は2000ですので、全然足りていませんでした。`4050` よりも大きい値にPriorityを設定することで、事象解消が可能になりました。

おわり。