Apache Camelのエラーハンドリング・リトライ処理を勉強してみた

# Apache Camelのエラーハンドリング・リトライ処理を勉強したのでメモ

Camelのエラー処理を勉強し始めるとonExceptionやdoTry等があり、違いがよく分からないなと思っていたら、以下の設計思想を見てからCamelの公式サイトを参照したらなんとなく理解できました。

[camel transactionのエラーハンドリング方法　－　設計思想](https://qiita.com/jian-feng/items/392881ec99b1f7adfb6d)

説明は上の投稿に書いているので、比較表だけ作成してみました。

|  | onException | ErrorHandler | doTry/doCatch/doFinally |
|:-:|:-:|:-:|:-:|
| 場所 | CamelContext, route(ルートで発生した例外がすべてtrapされる) | CamelContext, route(ルートで発生した例外がすべてtrapされる)  | route(doTryの中だけ) |
| リトライ  | ○ | ○ | ×(onException/ErrorHandlerと組み合わせる？) |
| 特定の例外の指定※1 | ○ | × | ○ |

※1：例えば、IOExceptionと他のExceptionで処理を変えることができる。


# まずはonException

CamelではonException()メソッドを使用して、例外クラスごとにエラー処理を実行できます。

例えば、以下のように記述すると、Exceptionから派生したクラスの例外が発生すると、例外をキャッチし、logを出力しています。

```xml
		<onException>
			<exception>java.lang.Exception</exception>
			<log message="global exception" />
		</onException>
```

onExceptionは、記述する箇所により以下の2つのスコープがあります。

・グローバル
　ルートの外にonExceptionを書くとグローバルスコープになります。特定のルートで例外が処理されない場合は、グローバルスコープで処理されます。
・特定のルート
　特定のルートでonExceptionを書いた場合、そのルートで発生した例外が処理されます。
　ただし、特定のルートにonExceptionがあったとしても、そこから呼び出され子ルートで例外が発生しても、呼び出し元のonExceptionで例外は処理されません。

以下ではparentRouteにonExceptionがあり、childRouteでthrowExceptionを使用して例外を発生させています。childRouteではonExceptionがないので、グローバルのonExceptionがあればそちらで例外が処理されます。呼び出し元のparentRouteのonExceptionは処理されないことに注意。

```xml
		<route id="parentRoute">
			<from uri="timer://runOnce?repeatCount=1&amp;delay=1000" />
			<to uri="direct:child" />
			<onException>
				<exception>java.lang.Exception</exception>
				<log message="parent end" />
			</onException>
		</route>

		<route id="childRoute">
			<from uri="direct:child" />
			<throwException
				exceptionType="java.lang.IllegalArgumentException" message="child 1" />
		</route>
```

# リトライ処理

デフォルトでは、例外が発生してもCamelはリトライしません。
例外発生時にリトライしたい場合、redeliveryPolicyProfileを使用します。

以下ではredeliveryPolicyProfileでリトライ処理を定義しています。
・redeliveryPolicyProfileのmaximumRedeliveriesで5回リトライする、redeliveryDelayで1秒ごとにリトライすることを定義しています。
・retryAttemptedLogLevelにWARNを設定することでリトライ時にWARNレベルのログが出力されます。（デフォルトはDEBUG）
・handledをtrueに設定することで、現在の例外が再スローされなくなります。

```xml
		<redeliveryPolicyProfile
			id="testRedeliveryPolicyProfile" retryAttemptedLogLevel="WARN"
			maximumRedeliveries="5" redeliveryDelay="1000" />

		<onException
			redeliveryPolicyRef="testRedeliveryPolicyProfile">
			<exception>java.lang.Exception</exception>
			<handled>
				<constant>true</constant>
			</handled>
			<log message="global exception" />
		</onException>

```

リトライは回数やリトライ間隔などで細かな指定ができます。
以下では、delayPatternで"0:1000;3:5000"と設定することで、
・1～3回目までは1秒ごとにリトライ
・4回目以降は5秒ごとにリトライ
というリトライ処理になります。

```xml
		<redeliveryPolicyProfile
			id="testRedeliveryPolicyProfile" retryAttemptedLogLevel="WARN"
			maximumRedeliveries="5" delayPattern="0:1000;3:5000" />
```

# dead letter channelパターンを使用する

dead letter channelパターンは、メッセージの配信でリトライしても最終的に失敗した場合、配信できなかったメッセージをアーカイブするデッドレターチャネルに送信するパターンです。

onExceptionなら、「\<to uri="seda:error" />」を追加してSEDAに入れるだけですので簡単です。

ErrorHandlerを使用する場合は以下のようになります。
・type="DeadLetterChannel"とし、deadLetterUriに宛先（"seda:error"）を指定する。
・ルートにerrorHandlerを設定する。（errorHandlerRef="customErrorHandler"）
・errorChannelRouteのルートはseda:errorに配信されたデッドレターメッセージをログに出力している。

```xml
	<camelContext
		xmlns="http://camel.apache.org/schema/spring">

		<errorHandler id="customErrorHandler"
			type="DeadLetterChannel" deadLetterUri="seda:error"
			useOriginalMessage="true" redeliveryPolicyRef="testRedeliveryPolicyProfile" />

		<redeliveryPolicyProfile
			id="testRedeliveryPolicyProfile" retryAttemptedLogLevel="WARN"
			maximumRedeliveries="5" delayPattern="0:1000;3:2000" />

		<route id="parentRoute" errorHandlerRef="customErrorHandler">
			<from uri="timer://runOnce?repeatCount=1&amp;delay=1000" />
			<throwException
				exceptionType="java.lang.IllegalArgumentException" message="child 1" />
			<log message="parent end" />
		</route>

		<route id="errorChannelRoute">
			<from uri="seda:error" />
			<log message="seda error" />
			<to uri="log:customError" />
		</route>
	</camelContext>
```

# 最後にdoTry/doCatch/doFinally

最後にdoTry/doCatch/doFinallyですが、これは普段書いているプログラムとも似ているのでわかりやすいです。
doTryの中で、throwExceptionを使用して例外を発生させています。doCatchは2つあり、発生したIllegalArgumentExceptionは一方のdoCatchでのみ処理されます。

```xml
	<camelContext
		xmlns="http://camel.apache.org/schema/spring">

		<route id="parentRoute">
			<from uri="timer://runOnce?repeatCount=1&amp;delay=1000" />
			<doTry>
				<log message="parent route start" />
				<throwException
					exceptionType="java.lang.IllegalArgumentException"
					message="parent 1" />
				<doCatch>
					<exception>java.io.IOException</exception>
					<exception>java.lang.IllegalStateException</exception>
					<to uri="seda:error" />
				</doCatch>
				<doCatch>
					<exception>java.lang.Exception</exception>
					<to uri="log:doCatch?showHeaders=true" />
					<to uri="seda:error" />
				</doCatch>
				<doFinally>
					<log message="parent route end" />
				</doFinally>
			</doTry>
		</route>

		<route id="errorChannelRoute">
			<from uri="seda:error" />
			<to uri="log:customError?showHeaders=true" />
		</route>
	</camelContext>
```

# 参考

- [10分で基礎をマスターするApache Camel](https://qiita.com/daikuro/items/2125c399d721200e8bac)
- [Apache Camel Exception Clause](https://camel.apache.org/manual/latest/exception-clause.html)
- [camel transactionのエラーハンドリング方法](https://qiita.com/jian-feng/items/392881ec99b1f7adfb6d)