Retryユーティリティ
癒しを求めてSVNリポジトリを徘徊していたら、昔書いた懐かしいコードを発見したので公開してみます。
- 任意の処理を実行し、
- 処理の戻り値または例外を評価して、リトライする必要がある場合、
- 指定された回数だけ再試行する
ための、ユーティリティです。
/** * リトライを行なうための機能セット。 */ class Retry { /** * リトライする。 * * <ul> * <li>{@link Logic}を実行し、発生した例外または戻り値がリトライ対象である場合、リトライする。</li> * <li>リトライ対象かどうかは、{@link Condition}で評価する。</li> * <li> * 指定回数リトライしても、{@link Logic}がリトライするべき結果を返す場合、 * 最後の実行結果を返す。または、最後に発生した例外をスローする。 * </li> * <li> * {@link Logic}の戻り値、または発生した例外がリトライ対象でない場合、 * 戻り値をそのまま返し(または例外をスローし)、即座に復帰する。 * </li> * </ul> * * @param <R> 戻り値型 * @param <E> 発生する例外の基底クラス * * @param retryCount リトライ回数 * @param condition リトライ条件 * @param logic 処理 * @return 最終的な処理の実行結果 * @throws E リトライしても発生した最後の例外。 */ static final <R, E extends Exception> R retry( int retryCount, Condition<R, E> condition, Logic<R, E> logic ) throws E { // 最後に実行した処理と発生した例外。 // リトライ回数が指定回数を超えた場合、この値を返す。 R lastresult = null; E lastException = null; for ( int i = 0; i <= retryCount; i++ ) { try { lastException = null; lastresult = null; lastresult = logic.call(i); if ( !condition.isRetry(lastresult) ) { return lastresult; } } catch ( ContinueException e ) { // リトライ } catch ( BreakException e ) { // リトライを即刻中止 try { return (R) e.get(); } catch ( Throwable t ) { if ( t instanceof RuntimeException ) { throw (RuntimeException) t; } if ( t instanceof Error ) { throw (Error) t; } throw (E) t; } } catch ( Exception ex ) { // catch で型パラメータは使えないので仕方なく。 lastException = (E) ex; if ( !condition.isRetry((E) ex) ) { throw (E) ex; } } } // リトライ回数が指定値を超えた場合、最後の実行結果を返す。 if ( lastException != null ) { throw lastException; } return lastresult; } /** * リトライする条件 * * @param <R> 戻り値型 * @param <E> 発生する例外の基底クラス */ static interface Condition<R, E extends Exception> { /** * 正常終了した結果をもとに、リトライするか評価する。 * @param result 実行結果 * @return リトライする場合true */ boolean isRetry( R result ); /** * 発生した例外をもとに、リトライするか評価する。 * @param error 発生した例外 * @return リトライする場合true */ boolean isRetry( E error ); } /** * リトライする処理 * * @param <R> 戻り値型 * @param <E> 発生する例外の基底クラス */ static interface Logic<R, E extends Exception> { /** * 処理を実行 * @param count リトライした回数 * @return 実行結果 * @throws E 発生する例外 */ R call( int count ) throws E; } /** * リトライする処理の抽象基底クラス * * @param <R> 戻り値型 * @param <E> 発生する例外の基底クラス */ static abstract class AbstractLogic<R, E extends Exception> implements Logic<R, E> { /** * 処理を中断する。 * @param result 処理の戻り値 */ void break_(R... result) { throw new BreakException( result != null && result.length > 0 ? result[0] : null ); } /** * 処理を中断する。 * @param error 処理で発生したエラー */ void break_( E error ) { throw new BreakException( error ); } /** * 以降の処理を中断し、次のループへ。 */ void continue_() { throw new ContinueException(); } } /** * 処理の中断を示す例外 * 例外派生クラスはパラメータ化できない */ private static final class BreakException extends RuntimeException { final Object returnValue; final Throwable t; BreakException( Object returnValue ) { super(); this.returnValue = returnValue; this.t = null; } BreakException( Throwable t ) { super(t); this.returnValue = null; this.t = t; } /** * 結果を取得する。 * @return 結果 * @throws Throwable 処理が例外により中断された場合。 */ Object get() throws Throwable { if ( t != null ) throw t; return returnValue; } }; /** * 処理の継続を示す例外 */ private static final class ContinueException extends RuntimeException {}; }
使い方。
// 処理中で実行時例外が発生したらリトライする条件 Condition<String, Exception> ifRuntimeException = new Condition<String, Exception>() { public boolean isRetry ( String result ) { return false; } public boolean isRetry ( Exception error ) { return error instanceof RuntimeException; } }; // Logicの処理を実行し、実行時例外が発生する場合は、5回までリトライする。 String result = retry( 5, ifRuntimeException, new AbstractLogic<String, Exception>() { public String call ( int count ) throws Exception { // 処理 System.out.println( "count : " + count ); if ( count < 3 ) throw new RuntimeException("test"); return String.valueOf( count ); } } ); System.out.println( "result : " + result );
実行結果です。3回実行時例外が発生するので再試行し、4回目の結果を返しています。
count : 0 count : 1 count : 2 count : 3 result : 3
ところで、せっかく汎用的に作ったのに、カレントのソースにはシステム依存のコード(リトライしたら○×サービスを再起動する、とか)が追加されていて、ちょいげんなり。ロジック内の処理として実装するのでは問題があったんだろうか?