環境
- Java 8
- CDI 2.0
- Microprofile Fault Tolerance 2.0
事象
Microprofile Fault Tolerance 2.0 Circuit Breakerのアノテーションと、独自に用意した 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;
}
}
}
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Foo {}
@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の場合のサーキットブレーカ発動を回避することができました。
@Interceptor
@Dependent
@Foo
// @Priority(Interceptor.Priority.APPLICATION)
@Priority(5000)
public class FooInterceptor {
// 省略
}
原因
本事象は、@CircuitBreaker
が呼び出すInterceptorのPriorityよりも、 FooInterceptor
のPriorityが高かったために発生しました。
実行されるべきInterceptorが複数存在するときの実行順序は、それらが持つPriorityにより決まります。 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の仕様では、以下のように記述されており、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を設定することで、事象解消が可能になりました。
おわり。