差異處
這裏顯示兩個版本的差異處。
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~~ |