/*
* Computoser is a music-composition algorithm and a website to present the results
* Copyright (C) 2012-2014 Bozhidar Bozhanov
*
* Computoser is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Computoser is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Computoser. If not, see <http://www.gnu.org/licenses/>.
*/
package com.music.util;
import java.util.concurrent.Callable;
/**
* Class that provides retrying functionality. Example:
*
* <code>
* Callable<String> callable = new Callable<String>() {..};
* String result = RetryableOperation.create(callable).retry(5, IOException.class);
* </code>
* @author bozho
* @param <T> the return type of the operation
*/
public class RetryableOperation<T> {
private Callable<T> callable;
private Runnable runnable;
private boolean exponentialBackoff;
private int backoffInterval = 500;
private ExceptionHandler<? super Exception> exceptionHandler;
/**
* Create a retryable operation based on a Callable instance. The return
* type of retry(..) is the type parameter of the Callable instance.
*
* @param callable
* @return a new instance of RetryableOperation
*/
public static <T> RetryableOperation<T> create(Callable<T> callable) {
return new RetryableOperation<T>().withCallable(callable);
}
/**
* Creates a retryable operation based on a Runnable instance. In this case
* the retry(..) method always returns null.
*
* @param runnable
* @return a new instance of RetryableOperation
*/
public static RetryableOperation<?> create(Runnable runnable) {
return new RetryableOperation<Object>().withRunnable(runnable);
}
/**
* Retries the operation. Retrying happens regardless of the exception thrown.
*
* @param retries number of retries before the exception is thrown to the caller
* @return the result of the operation (null if Runnable is used instead of Callable)
* @throws Exception the exception that occurred on the last attempt
*/
public <E extends Exception> T retry(int retries) throws Exception {
return retry(retries, Exception.class);
}
/**
* @param retries
* number of retries before the exception is thrown to the caller
* @param retryForException
* retry only if the exception thrown by the operation is of this
* type. Otherwise rethrow the exception immediately, wrapped in
* a RuntimeException
* @return the result of the operation (null if Runnable is used instead of
* Callable)
* @throws E
* the exception that is expected to cause the operation to
* retry. It is only if the last retry attempt fails
*/
public <E extends Exception> T retry(int retries, Class<E> retryForException) throws E {
if (callable == null && runnable == null) {
throw new IllegalStateException("Either runnable or callable must be set");
}
for (int i = 0; i < retries; i++) {
try {
if (exponentialBackoff && i > 0) {
int sleepTime = (int) ((Math.pow(2, i) - 1) / 2) * backoffInterval;
Thread.sleep(sleepTime);
}
if (callable != null) {
return callable.call();
} else if (runnable != null) {
runnable.run();
return null;
}
} catch (Exception e) {
// if there is a registered exception handler, perform the exception handling
// and optionally return, if the handler indicates that
if (exceptionHandler != null
&& exceptionHandler.getExceptionClass().isInstance(e)
&& !exceptionHandler.handle(exceptionHandler.getExceptionClass().cast(e))) {
return null;
}
// if the exception is the expected type, do nothing (i.e. retry the next iteration)
// if this is the last iteration, rethrow the exception
if (retryForException.isInstance(e)) {
E ex = retryForException.cast(e);
if (i == retries - 1) {
throw ex;
}
} else {
// if the exception is not the expected one, throw a runtime exception to the caller
throw new RuntimeException(e);
}
}
}
// can't be reached - in case of failure on the last iteration the exception is rethrown
return null;
}
private RetryableOperation<T> withCallable(Callable<T> callable) {
this.callable = callable;
return this;
}
private RetryableOperation<T> withRunnable(Runnable runnable) {
this.runnable = runnable;
return this;
}
/**
* Sets an optional exception handler to this retryable operation. The
* handler can choose whether to proceed with retrying or stop immediately.
*
* @param exceptionHandler
* @return the RetryableOperation instance
*/
@SuppressWarnings("unchecked")
public RetryableOperation<T> withExceptionHandler(ExceptionHandler<? extends Exception> exceptionHandler) {
this.exceptionHandler = (ExceptionHandler<? super Exception>) exceptionHandler;
return this;
}
public RetryableOperation<T> withExponentialBackoff() {
this.exponentialBackoff = true;
return this;
}
public RetryableOperation<T> withExponentialBackoff(int backoffInterval) {
this.exponentialBackoff = true;
this.backoffInterval = backoffInterval;
return this;
}
/**
* Interface to be implemented whenever custom code is to be executed when an exception occurs while retrying
* @author bozho
*
* @param <E> the type of the exception to be handled by this handler
*/
public static interface ExceptionHandler<E extends Exception> {
/**
*
* @param ex
* @return true if retrying should proceed; false if it should stop immediately
*/
boolean handle(E ex);
/**
*
* @return the exception class which this handler is capable of handling
*/
Class<E> getExceptionClass();
}
}