差異處

這裏顯示兩個版本的差異處。

連向這個比對檢視

java:basic:reusefulretry [2017/08/19 23:33]
java:basic:reusefulretry [2023/06/25 09:48] (目前版本)
行 1: 行 1:
 +{{tag>​java}}
 +====== 可重複使用的Retry ======
 +===== Why... =====
 +當系統發生了例外情況時,夠強健的系統會重新嘗試(retry)發生問題的操作,最常見的例子就是連線中斷的重連。也有可能會另尋其它路徑,像是使用備援的系統或資料來源。一開始我採用了while/​for loop的方式做retry,然而當這樣的程式碼夠多後,看了也挺令人厭煩的。Spring有提供RetryTemplate,讓你可以實做想要的Retry。但Spring實在太大包了,除非系統中一定會用到Spring,否則要包這個東西進去,也是挺OOXX的。\\
  
 +===== I Thinking & Trying =====
 +於是我開始嘗試著造輪子。我參考Reference的三篇文章,並設計了一個折中的方式,可以滿足大部分的需求(我需要的需求)。
 +  - 設定重試次數
 +  - 設定重試條件
 +  - 設定重試延遲時間
 +  - 可做Alternative操作
 +
 +我參考了Reference中的三篇文章,設計了一個**我認為**好擴充與方便使用的RetryableTask類別,讓我可以將這些繁瑣的動作給包裝起來:​\\
 +{{:​java:​basic:​class-diagram-retryabletask.png?​750|}}\\
 +
 +概念相當簡單:​
 +  * RetryableTask負責執行這些retry的動作,它依賴於Callable、ISleepStrategy、IRetryablePolicy類別。
 +  * Callable類別讓Programmer將**操作**給包裝起來,管你是要從DB還是從檔案取資料,這裡就是提供**功能**的操作流程。
 +  * ISleepStrategy負責讓RetryableTask知道每次Retry時,需要先等待幾秒鐘。目前提供了BasicSleepStrategy(固定時間)與VariableSleepStrategy(變化時間)。
 +  * IRetryablePolicy會根據**每次操作的結果**,決定是否要Retry。這裡使用了Composite Pattern,讓你可以使用多個Policy去控制Retry策略。目前提供了AttemptRetryablePolicy(最大次數)、NullRetryablePolicy(不可為NULL)、ExceptioRetryablePolicy(例外情形)三種策略。
 +
 +===== Programming =====
 +(懶的看可以自行下載程式碼閱讀{{:​java:​basic:​retryabletask.zip|Download}})
 +==== IRetryableTask ====
 +首先讓我們看看RetryableTask的member與constructor,預設是使用BasicSleepStrategy與ExceptioRetryablePolicy,當然也可以透過set method去更改。功能執行的主體則是透過Callable,Client必須把它的操作流程先實做好再丟進來。
 +<code java>
 +private Callable<​T>​ mCallable = null;
 +private Object mResult = null;
 +private ISleepStrategy mSleepStrategy = new BasicSleepStrategy();​
 +private IRetryablePolicy mRetryablePolicy = new ExceptioRetryablePolicy();​
 +
 +public RetryableTask(Callable<​T>​ callable){
 + mCallable = callable;
 +}
 +</​code>​
 +
 +接著是最核心的部分。基本上就是透過ISleepStrategy與IRetryablePolicy去控制流程,透過mCallable.call()去執行主要功能。萬一都重試失敗,就將結果回傳或將例外往上丟。(結果也許會由mCallable.call()回傳一個Default value)
 +<code java>
 +@Override
 +public T call() throws Throwable {
 + boolean isFirstTime = true;
 + do {
 + if(!isFirstTime && mSleepStrategy != null){
 + ThreadUtil.sleep(mSleepStrategy.getSleepTime());​
 + }
 + try {
 + T result = mCallable.call();​
 + mResult = result;
 + } catch( Exception e ){
 + mResult = e;
 + }
 + } while( isNeedToRetry(mResult) );
 +
 + if( mResult instanceof Throwable ){
 + throw (Throwable)mResult;​
 + }
 + return (T)mResult;
 +}
 +
 +private boolean isNeedToRetry(Object aData){
 + if( mRetryablePolicy != null ){
 + return mRetryablePolicy.needToRetry(aData);​
 + }
 + return false;
 +}
 +</​code>​
 +==== ISleepStrategy ====
 +ISleepStrategy就是實做每次Retry時,你要Sleep多久的規則而已。
 +<code java>
 +public class VariableSleepStrategy implements ISleepStrategy {
 +
 + private int mCurrentIndex = 0;
 + private long[] mSleepTimes;​
 + private int mMaxLenghth = 0;
 +
 + public VariableSleepStrategy(long[] sleepTimes){
 + mMaxLenghth = sleepTimes.length;​
 + mSleepTimes = new long[mMaxLenghth];​
 + System.arraycopy(sleepTimes,​ 0, mSleepTimes,​ 0, mMaxLenghth);​
 + }
 +
 + @Override
 + public long getSleepTime() {
 + if( mCurrentIndex == mMaxLenghth ){
 + throw new RuntimeException("​Over the max length."​);​
 + }
 + return mSleepTimes[mCurrentIndex++];​
 + }
 +}
 +</​code>​
 +==== IRetryablePolicy ====
 +IRetryablePolicy會根據執行結果決定是否要Retry,我以ExceptionRetryablePolicy為例。ExceptionRetryablePolicy提供三個建構子,一個支援如果執行結果為Exception類別或子類別就要Retry;另外兩個會根據你給定的例外類別列表,結果有在其中才Retry。
 +<code java>
 +public class ExceptionRetryablePolicy implements IRetryablePolicy {
 +
 + private List<​Class<?​ extends Throwable>>​ mExceptinList = null;
 +
 + public ExceptionRetryablePolicy() {
 + this(Exception.class);​
 + }
 +
 + public ExceptionRetryablePolicy(Class<?​ extends Throwable>​ throwableClass) {
 + mExceptinList = new ArrayList<​Class<?​ extends Throwable>>​();​
 + mExceptinList.add(throwableClass);​
 + }
 +
 + public ExceptionRetryablePolicy(List<​Class<?​ extends Throwable>>​ exceptinList) {
 + mExceptinList = exceptinList;​
 + }
 +
 + @Override
 + public boolean needToRetry(Object data) {
 + if( data == null || mExceptinList == null )
 + return false;
 + for( Class<? extends Throwable>​ throwableClass : mExceptinList ){
 + if( throwableClass.isInstance(data)){
 + return true;
 + }
 + }
 + return false;
 + }
 +}
 +</​code>​
 +CompositeRetryablePolicy支援多個策略,像是你可以同時支援最大次數、例外情況或NULL情況。實做就是去呼叫各Policy的needToRetry去決定。
 +<code java>
 +public class CompositeRetryablePolicy implements IRetryablePolicy {
 +
 + private List<​IRetryablePolicy>​ mPolicyList = null;
 +
 + public CompositeRetryablePolicy(){
 +
 + }
 +
 + public CompositeRetryablePolicy(List<​IRetryablePolicy>​ policyList){
 + mPolicyList = policyList;
 + }
 +
 + @Override
 + public boolean needToRetry(Object data) {
 + if( mPolicyList == null ){
 + return false;
 + }
 + boolean needRetry = !mPolicyList.isEmpty();​
 + for( IRetryablePolicy policy : mPolicyList ){
 + if(!policy.needToRetry(data)){
 + needRetry = false;
 + }
 + }
 + return needRetry;
 + }
 +}
 +</​code>​
 +NullRetryablePolicy是在結果為NULL時,去做Retry;AttemptRetryablePolicy則是用來控制最大的重試次數。
 +===== Testing =====
 +我透過Powermock,並實做一個Alternative retry給大家看看。首先讓我們mock要呼叫的method:​ userDao1.getUser()與userDao2.getUser(),假設userDao1 from DB,userDao2 from file。userDao1為第一次執行使用,會拋出一個例外;userDao2在第二次使用,會回傳正確結果。
 +<code java>
 +IUser user_expect = PowerMock.createMock(IUser.class);​
 +
 +String errorMsg = "​Testing error msg";
 +final IUserDao userDao1 = PowerMock.createStrictMock(IUserDao.class);​
 +userDao1.getUser(EasyMock.anyObject(String.class));​
 +PowerMock.expectLastCall().andThrow(new RuntimeException(errorMsg)).once();​
 +
 +final IUserDao userDao2 = PowerMock.createStrictMock(IUserDao.class);​
 +userDao2.getUser(EasyMock.anyObject(String.class));​
 +PowerMock.expectLastCall().andReturn(user_expect).once();​
 +
 +PowerMock.replayAll();​
 +</​code>​
 +
 +Callable call()的實做透過userDao1與userDao2去交互執行,若userDao1.getUser執行失敗就會用userDao2.getUser。像要取得一個local port,也許就可以透過遞增或遞減port number去實做。
 +<code java>
 +Callable<​IUser>​ platformUtil = new Callable<​IUser>​() {
 + private boolean switchFlag = true;
 + @Override
 + public IUser call() throws Exception {
 + switchFlag = !switchFlag;​
 + if( !switchFlag )
 + return userDao1.getUser("​1234"​);​
 + else {
 + return userDao2.getUser("​1234"​);​
 + }
 + }
 +};
 +</​code>​
 +
 +RetryableTask的部分使用了AttemptRetryablePolicy與ExceptionRetryablePolicy,要求重試次數小於3且發生例外時要Retry。最後我們期望的是能夠透過userDao2.getUser取得與user_expect相同的結果,因此使用了PowerMock.verifyAll()去確認那些mock object都有被呼叫到。
 +<code java>
 +IRetryableTask<​IUser>​ retryableTask = new RetryableTask<​IUser>​(platformUtil);​
 +IRetryablePolicy compositeRetryablePolicy = new CompositeRetryablePolicy(Arrays.asList(new IRetryablePolicy[]{
 + new AttemptRetryablePolicy(3),​
 + new ExceptionRetryablePolicy()
 +}));
 +retryableTask.setRetryablePolicy(compositeRetryablePolicy);​
 +try {
 + assertEquals(user_expect,​ retryableTask.call());​
 +} catch (Throwable e) {
 + fail();
 +}
 +
 +PowerMock.verifyAll();​
 +</​code>​
 +===== Summary =====
 +就目前的需求,這是我所能想到的設計。也許可以把更多的東西做抽像化來增加更多的擴充性,但是目前這樣就能滿足我了。
 +
 +友藏內心獨白:​ 有先請教過學姐,再稍稍做修改。也許還達不到最令人滿意的設計。
 +===== Reference =====
 +  * [[http://​teddy-chen-tw.blogspot.tw/​2010/​03/​5-spare-handler.html|搞笑談軟工 - 敏捷式例外處理設計 (5):我到底哪裡做錯之 spare handler]]
 +  * [[http://​static.springsource.org/​spring-batch/​apidocs/​org/​springframework/​batch/​retry/​support/​RetryTemplate.html|Spring RetryTemplate]]
 +  * [[http://​fahdshariff.blogspot.tw/​2009/​08/​retrying-operations-in-java.html|Retrying perations in java]]
 +=====    =====
 +----
 +\\
 +~~DISQUS~~