Apache Camel源碼研究之Error Handler

在做企業應用集成解決方案中,錯誤處理這一塊是非常重要的一個環節,而相較於於自身原因導致的錯誤,調用遠程服務時出現錯誤纔是最讓人頭疼的,在人們發揮聰明才智總結出的各類解決方案中,重試無疑是最容易想到且實施的方案之一。

1. 概述

本文將嘗試就Apache Camel的Error Handler架構中內置的Retry機制進行研究,確保在對其進行應用時候做到心中有底,胸有成竹。

2. 源碼解讀

首先列出有關疑問:

  1. Apache Camel中Retry是如何配置的?
  2. 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源碼研究之啓動,我們可以推斷如下:

  1. 當前這個自定義的Processor節點是肯定會被Wrap進ErrorHandler Processor(即使你沒有顯式配置ErrorHandler,都會有一個默認的),除非當前節點爲doTry()doCatch()doFinally()onException()等節點。
  2. Apache Camel使用專門的DefaultErrorHandlerBuilder來構建ErrorHandler實例(通過實現接口ErrorHandlerFactory接口),在接口實現中將會回調基類ErrorHandlerBuilderSupportconfigure()方法,爲構建出的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源碼研究之啓動,我們嘗試總結:

  1. 對於每個用戶自定義的Processor,Apache Camel都會將其Wrap爲Channel實例,在這個過程中Apache Camel會爲其附加一系列額外內置的功能性Processor,最終形成由Processor組成的千層餅式的鏈表數據結構,用戶自定義Processor位於該千層餅結構的正中心。
  2. 除非進行專門的配置,否則對於每個用戶自定義的Processor,在以上的千層餅式的結構中必然有一層是DefaultErrorHandler。(從上述堆棧圖中也能看出一二)
  3. 因此對於每個用戶自定義Processor,其失敗之後的Retry都將是獨立的,Retry只會從失敗的Processor開始,而不是從頭再來。這正是我們最開始提出的問題之二的解答。
  4. DefaultErrorHandler類直接繼承自RedeliveryErrorHandler,從父類的名稱就能看出其天然就具有Retry的能力。
  5. 默認的Retry時間間隔爲 1 秒,最大值不能超過60秒,這個默認值來自 RedeliveryPolicy類中字段redeliveryDelay

3. 額外說明

Apache Camel還提供了一些額外的輔助來追蹤,強化Error Handler功能。

  1. 接口LifecycleStrategy定義了契約方法onErrorHandlerAddonErrorHandlerRemove,用於監視ErrorHandler的添加和移除。
  2. Apache Camel還會播發ExchangeRedeliveryEvent 事件(通過 EventHelper.notifyExchangeRedelivery ),因此我們也可以通過添加監聽器的方式來加入自定義邏輯。

4. Links

  1. 《Apache Camel Developer’s Cookbook》P199
  2. 《Camel In Action》P129
  3. Office Site - Error Handler
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章