在做企業應用集成解決方案中,錯誤處理這一塊是非常重要的一個環節,而相較於於自身原因導致的錯誤,調用遠程服務時出現錯誤纔是最讓人頭疼的,在人們發揮聰明才智總結出的各類解決方案中,重試無疑是最容易想到且實施的方案之一。
1. 概述
本文將嘗試就Apache Camel的Error Handler架構中內置的Retry機制進行研究,確保在對其進行應用時候做到心中有底,胸有成竹。
2. 源碼解讀
首先列出有關疑問:
- Apache Camel中Retry是如何配置的?
- Apache Camel中Retry從何處開始?即是從失敗的Processor節點開始,亦或是從頭開始?或是其它?,以及Apache Camel是如何實現的?
關於以上疑問,依然是先給出本次的用例:
CamelTestUtil.defaultPrepareTest2(new RouteBuilder() {
@Override
public void configure() throws Exception {
// 這個是 context scope 層面的配置,對所有的route都起作用
// 如果要針對單個route進行配置, 則需要在該route的from()定義之後進行相應的配置, 即 : from("xxx").errorHandler(errorHandlerBuilder)
errorHandler( //
defaultErrorHandler() //
.retryWhile( //
simple("${header.CamelRedeliveryCounter} < 3 " //
+ "or ${date:now:EEE} contains 'Tue'") //
) //
.retryAttemptedLogLevel(LoggingLevel.INFO) //
.logStackTrace(true) //
.logRetryStackTrace(true) //
);
from("stream:in?promptMessage=Enter something:")//
.process(MessageDetailReportProcessor.me)//
// 我們自定義的Processor實現在Camel內部使用 DelegateSyncProcessor 進行統一包裝, 避免直接接觸,建立中間層,增加系統彈性。
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
final Integer camelRedeliveryCounter = exchange.getIn().getHeader(
Exchange.REDELIVERY_COUNTER, int.class);
Console.log("當前線程名稱: " + Thread.currentThread().getName());
if(camelRedeliveryCounter < 2){
throw new RuntimeException("LQ" + camelRedeliveryCounter);
}
}
});
// ====================== 另外一種簡約版配置 —— 當發生特定異常時候進行重試操作
// ====================== 需要注意的是此項配置只針對該配置出現之後的節點, 表現在以下配置中就是在ExceptionProcessor2中拋出RuntimeException類型異常會觸發重試, 而ExceptionProcessor1中則不會。
// from("stream:in?promptMessage=Enter something:") //
// .process(new ExceptionProcessor1())//
// .onException(RuntimeException.class).maximumRedeliveries(2) //
// //.retryWhile(retryWhile) //
// //.logRetryStackTrace(true)
// .end() // 必須!
// .process(new ExceptionProcessor2())//
// .to("stream:out");
}
});
2.1 啓動時
首先讓我們來看看在初始化階段,Apache Camel是如何將Retry功能組裝進Camel執行鏈條中的。
以上用例啓動之後,得到以下堆棧:
結合之前的博客 Apache Camel源碼研究之啓動,我們可以推斷如下:
- 當前這個自定義的
Processor
節點是肯定會被Wrap進ErrorHandler
Processor(即使你沒有顯式配置ErrorHandler
,都會有一個默認的),除非當前節點爲doTry()
,doCatch()
,doFinally()
,onException()
等節點。 - Apache Camel使用專門的
DefaultErrorHandlerBuilder
來構建ErrorHandler
實例(通過實現接口ErrorHandlerFactory
接口),在接口實現中將會回調基類ErrorHandlerBuilderSupport
的configure()
方法,爲構建出的DefaultErrorHandler
實例附加上用戶配置的onException()
。所以是可以同時配置onException()
和retryWhile()
的 。
2.2 執行時
以上用例運行起來之後,跟蹤堆棧我們找到DefaultErrorHandler
,關於DefaultErrorHandler
我們發現其實現相當簡單,其主要邏輯全部位於基類RedeliveryErrorHandler
中,包括最重要的AsyncProcessor
接口實現。
// RedeliveryErrorHandler.process()
// RedeliveryErrorHandler對接口AsyncProcessor的實現
/**
* Process the exchange using redelivery error handling.
*/
public boolean process(final Exchange exchange, final AsyncCallback callback) {
final RedeliveryData data = new RedeliveryData();
// do a defensive copy of the original Exchange, which is needed for redelivery so we can ensure the
// original Exchange is being redelivered, and not a mutated Exchange
data.original = defensiveCopyExchangeIfNeeded(exchange);
// 看到這個 while(true) 基本就可以認定重試正是在這段邏輯塊裏實現的
// use looping to have redelivery attempts
while (true) {
...
// did previous processing cause an exception?
// 按照默認實現, 就是簡單地判斷上一個processor執行時候是否發生異常
boolean handle = shouldHandleException(exchange);
if (handle) {
// 該方法中會修改 data.redeliveryCounter 的值, 也正是基於這個值,使得 RedeliveryErrorHandler.process() 中開始進行重試前的準備工作
handleException(exchange, data, isDeadLetterChannel());
// 回調配置項
onExceptionOccurred(exchange, data);
}
// compute if we are exhausted, and whether redelivery is allowed
// 檢測Retry是否已經達到約定的閾值, 以及是否允許Retry
// Exhausted : 筋疲力盡的
// Redelivery : 重試
boolean exhausted = isExhausted(exchange, data);
boolean redeliverAllowed = isRedeliveryAllowed(data);
// if we are exhausted or redelivery is not allowed, then deliver to failure processor (eg such as DLC)
if (!redeliverAllowed || exhausted) {
...
// we are breaking out
return sync;
}
// 這個值唯一的修改機會就是在上面的handleException()方法中
// 按照默認實現, 我們可以推斷只有在processor發生異常的時候, Retry纔會發生
if (data.redeliveryCounter > 0) {
// calculate delay
data.redeliveryDelay = determineRedeliveryDelay(exchange, data.currentRedeliveryPolicy, data.redeliveryDelay, data.redeliveryCounter);
// Retry之前是否需要等待一段特定的時間
if (data.redeliveryDelay > 0) {
// okay there is a delay so create a scheduled task to have it executed in the future
// 是否爲異步等待
if (data.currentRedeliveryPolicy.isAsyncDelayedRedelivery() && !exchange.isTransacted()) {
...
return false;
} else {
// async delayed redelivery was disabled or we are transacted so we must be synchronous
// as the transaction manager requires to execute in the same thread context
// 同步等待, 這裏注意上方的官方註釋, 筆者特意保留了
try {
// we are doing synchronous redelivery and use thread sleep, so we keep track using a counter how many are sleeping
redeliverySleepCounter.incrementAndGet();
data.currentRedeliveryPolicy.sleep(data.redeliveryDelay);
redeliverySleepCounter.decrementAndGet();
} catch (InterruptedException e) {
redeliverySleepCounter.decrementAndGet();
// we was interrupted so break out
exchange.setException(e);
// mark the exchange to stop continue routing when interrupted
// as we do not want to continue routing (for example a task has been cancelled)
exchange.setProperty(Exchange.ROUTE_STOP, Boolean.TRUE);
callback.done(data.sync);
return data.sync;
}
}
}
// =============== 以下三步均是做Retry之前的一系列準備工作
// prepare for redelivery
prepareExchangeForRedelivery(exchange, data);
// letting onRedeliver be executed
// 回調配置項
deliverToOnRedeliveryProcessor(exchange, data);
// emmit event we are doing redelivery
// 事件播發
EventHelper.notifyExchangeRedelivery(exchange.getContext(), exchange, data.redeliveryCounter);
}
// 正式回調當前processor
// process the exchange (also redelivery)
boolean sync = outputAsync.process(exchange, new AsyncCallback() {
public void done(boolean sync) {
// this callback should only handle the async case
if (sync) {
return;
}
// mark we are in async mode now
data.sync = false;
// if we are done then notify callback and exit
if (isDone(exchange)) {
callback.done(sync);
return;
}
// error occurred so loop back around which we do by invoking the processAsyncErrorHandler
// method which takes care of this in a asynchronous manner
// 本方法中的實現與本主體方法process()前半部分只有非常高的重複度
// 本方法中將進行callback的異步回調
processAsyncErrorHandler(exchange, callback, data);
}
});
// 區分同/異步執行邏輯
if (!sync) {
// the remainder of the Exchange is being processed asynchronously so we should return
return false;
}
// we continue to route synchronously
// if we are done then notify callback and exit
// 判斷是否不再需要繼續Retry
boolean done = isDone(exchange);
if (done) {
callback.done(true);
return true;
}
// error occurred so loop back around.....
}
}
雖然儘量刪除了較多與本次研究無關的細節,但最終被留存的代碼量依然比較大。以上代碼執行時候我們將得到以下堆棧:
結合之前的博客 Apache Camel源碼研究之啓動,我們嘗試總結:
- 對於每個用戶自定義的
Processor
,Apache Camel都會將其Wrap爲Channel
實例,在這個過程中Apache Camel會爲其附加一系列額外內置的功能性Processor
,最終形成由Processor
組成的千層餅式的鏈表數據結構,用戶自定義Processor
位於該千層餅結構的正中心。 - 除非進行專門的配置,否則對於每個用戶自定義的
Processor
,在以上的千層餅式的結構中必然有一層是DefaultErrorHandler
。(從上述堆棧圖中也能看出一二) - 因此對於每個用戶自定義
Processor
,其失敗之後的Retry都將是獨立的,Retry只會從失敗的Processor開始,而不是從頭再來。這正是我們最開始提出的問題之二的解答。 DefaultErrorHandler
類直接繼承自RedeliveryErrorHandler
,從父類的名稱就能看出其天然就具有Retry的能力。- 默認的Retry時間間隔爲 1 秒,最大值不能超過60秒,這個默認值來自
RedeliveryPolicy
類中字段redeliveryDelay
。
3. 額外說明
Apache Camel還提供了一些額外的輔助來追蹤,強化Error Handler功能。
- 接口
LifecycleStrategy
定義了契約方法onErrorHandlerAdd
,onErrorHandlerRemove
,用於監視ErrorHandler的添加和移除。 - Apache Camel還會播發
ExchangeRedeliveryEvent
事件(通過EventHelper.notifyExchangeRedelivery
),因此我們也可以通過添加監聽器的方式來加入自定義邏輯。
4. Links
- 《Apache Camel Developer’s Cookbook》P199
- 《Camel In Action》P129
- Office Site - Error Handler