/*
Copyright (c) 2007 Health Market Science, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
You can contact Health Market Science at info@healthmarketscience.com
or at the following address:
Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/
package com.healthmarketscience.rmiio;
import java.rmi.RemoteException;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
/**
* Utility class for automatically retrying remote method calls (which might
* fail for spurious reasons). Pretty much any remote method call should deal
* with RemoteExceptions because many transient failures (such as temporary
* network connection failures) can cause these exceptions, and the call may
* succeed when reattempted.
* <p>
* The major caveat for this class is the remote method call <b>must be
* idempotent</b>. This means, in essence, that repeated calls with the same
* arguments should generate the exact same results. An example of a
* non-idempotent call would be "remove $10 from my bank account". If this
* call were sent twice (because the first attempt seemed to fail), you could
* end up with $20 removed from your bank account. In order to solve this,
* you could add a unique sequence id to the call, "remove $10 from my bank
* account, seqId(5)", and the server could ignore the second call (assuming
* it was keeping track of the sequence ids that it had processed thus far).
* In other words, remote method calls are a lot harder to make than local
* method calls and much care should be taken when dealing with remote APIs.
* <p>
* Although RemoteRetry is an abstract class, there are a variety of simple
* implementations, as well as static instances of these implementations. See
* the implementations for more details.
* <p>
* Example usage:
* <pre>
*
* // use simple retry mechanism
* RemoteRetry retry = RemoteRetry.SIMPLE;
*
* // since we are using anonymous inner classes, these must be final
* final MyRemoteObject myRemoteObject;
* final int myArgument;
*
* // make a call with a return value
* Result res = retry.call(new RemoteRetry.Caller<Result>()
* {
* public Result call() throws RemoteException, MyException {
* return myRemoteObject.getResult(myArgument);
* }
* }, LOG, MyException.class, RemoteException.class);
*
* // make a call with no return value (use VoidCaller)
* retry.call(new RemoteRetry.VoidCaller()
* {
* public void call() throws RemoteException, MyException {
* return myRemoteObject.setValue(myArgument);
* }
* }, LOG, MyException.class, RemoteException.class);
*
* </pre>
* <p>
* Note that the various call() methods use generics to create methods with
* custom Exception signatures in addition to the custom return types.
*
*
* @author James Ahlborn
*/
public abstract class RemoteRetry
{
protected static final Logger LOG = LoggerFactory.getLogger(RemoteRetry.class);
/** RuntimeException class for overloading of exception types */
protected static final Class<RuntimeException> RUNTIME_CLASS =
RuntimeException.class;
/** instance of the {@link Never} retry strategy for general use. */
public static final RemoteRetry NEVER = new Never();
/** instance of the {@link Simple} retry strategy for general use. */
public static final RemoteRetry SIMPLE = new Simple();
/** instance of the {@link SimpleAlways} retry strategy for general use. */
public static final Always SIMPLE_ALWAYS = new SimpleAlways();
protected RemoteRetry() {}
/**
* Implementation of a simple backoff strategy:
* <ol>
* <li> First retry returns immediately
* <li> Retries 1 - 30 wait that many seconds each time before returning
* (after 1st retry wait 1 second, after 2nd retry wait 2 seconds...)
* <li> Retries > 30 wait 30 seconds each time before returning
* </ol>
*
* @param numRetries number of retries which have happended thus far
* @param log debug log
*/
protected static void simpleBackOff(int numRetries, Logger log)
{
if(numRetries == 0) {
// immediate retry first time
return;
}
long sleepTime = numRetries;
if(sleepTime > 30) {
sleepTime = 30;
}
try {
Thread.sleep(sleepTime * 1000);
} catch(InterruptedException ignored) {
// pass interrupt along
Thread.currentThread().interrupt();
if(log.isDebugEnabled()) {
log.debug("Caught exception while sleeping", ignored);
}
}
}
/**
* Implementation of the actual retry logic. Calls the call() method of the
* given Caller, returning results. All Throwables will be caught and
* shouldRetry() will be queried to see if the call should be reattempted.
* Iff shouldRetry() returns <code>true</code>, backoff() is called in order
* to allow the other end of the connection to have a breather and then the
* call() is reattempted (and the cycle repeats). Otherwise, the original
* Throwable is thrown to the caller.
*
* @param caller implementation of the actual remote method call
*/
protected final <RetType> RetType callImpl(Caller<RetType> caller, Logger log)
throws Throwable
{
int numTries = 0;
do {
try {
// attempt actual remote call
return caller.call();
} catch(Throwable e) {
// keep track of number of retries
++numTries;
// determine if caller wants to retry
if(!shouldRetry(e, numTries)) {
// guess not...
log.warn("Retry for caller " + caller + " giving up!");
throw e;
}
if(log.isDebugEnabled()) {
log.debug("Caller " + caller + " got exception, retrying", e);
}
// wait for a bit before retrying
backOff(numTries, log);
}
} while(true);
}
/**
* Wrapper for {@link #callImpl} which only throws RuntimeException.
*/
public <RetType> RetType call(Caller<RetType> caller)
{
return call(caller, LOG, RUNTIME_CLASS, RUNTIME_CLASS, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which only throws RuntimeException.
*/
public <RetType> RetType call(Caller<RetType> caller, Logger log)
{
return call(caller, log, RUNTIME_CLASS, RUNTIME_CLASS, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and one user
* defined Exception.
*/
public <RetType, ExType1 extends Throwable>
RetType call(Caller<RetType> caller,
Class<ExType1> throwType1)
throws ExType1
{
return call(caller, LOG, throwType1, RUNTIME_CLASS, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and one user
* defined Exception.
*/
public <RetType, ExType1 extends Throwable>
RetType call(Caller<RetType> caller,
Logger log,
Class<ExType1> throwType1)
throws ExType1
{
return call(caller, log, throwType1, RUNTIME_CLASS, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and two user
* defined Exceptions.
*/
public <RetType, ExType1 extends Throwable, ExType2 extends Throwable>
RetType call(Caller<RetType> caller,
Class<ExType1> throwType1,
Class<ExType2> throwType2)
throws ExType1, ExType2
{
return call(caller, LOG, throwType1, throwType2, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and two user
* defined Exceptions.
*/
public <RetType, ExType1 extends Throwable, ExType2 extends Throwable>
RetType call(Caller<RetType> caller,
Logger log,
Class<ExType1> throwType1,
Class<ExType2> throwType2)
throws ExType1, ExType2
{
return call(caller, log, throwType1, throwType2, RUNTIME_CLASS);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and three
* user defined Exceptions.
*/
public <RetType, ExType1 extends Throwable, ExType2 extends Throwable,
ExType3 extends Throwable>
RetType call(Caller<RetType> caller,
Class<ExType1> throwType1,
Class<ExType2> throwType2,
Class<ExType3> throwType3)
throws ExType1, ExType2, ExType3
{
return call(caller, LOG, throwType1, throwType2, throwType3);
}
/**
* Wrapper for {@link #callImpl} which throws RuntimeException and three
* user defined Exceptions.
*/
public <RetType, ExType1 extends Throwable, ExType2 extends Throwable,
ExType3 extends Throwable>
RetType call(Caller<RetType> caller,
Logger log,
Class<ExType1> throwType1,
Class<ExType2> throwType2,
Class<ExType3> throwType3)
throws ExType1, ExType2, ExType3
{
try {
return callImpl(caller, log);
} catch(Throwable e) {
throwCommonTypes(e);
throwIfMatchesType(e, throwType1);
throwIfMatchesType(e, throwType2);
throwIfMatchesType(e, throwType3);
throw handleNoMatches(e);
}
}
/**
* Checks the given exception against the given Exception type, throwing if
* the given exception is an instanceof the given type. Otherwise, returns.
*/
private static <ExType extends Throwable> void throwIfMatchesType(
Throwable throwable, Class<ExType> throwType)
throws ExType
{
if(throwType.isInstance(throwable)) {
throw throwType.cast(throwable);
}
}
/**
* Checks the given exception against a variety of common types (Error
* and RuntimeException), throwing if the given exception
* matches any of those types. Otherwise, returns.
*/
private static void throwCommonTypes(
Throwable throwable)
{
throwIfMatchesType(throwable, RuntimeException.class);
throwIfMatchesType(throwable, Error.class);
}
/**
* Cleanup method which returns an InternalError. This is necessary if the
* given exception did not match any of the given types (which should never
* happen).
*/
private static Error handleNoMatches(
Throwable throwable)
{
return (InternalError)
(new InternalError("Impossible exception thrown"))
.initCause(throwable);
}
/**
* Returns <code>true</code> if the caller should attempt to repeat the
* current remote method call given the number of previous reattempts.
*
* @param t throwable thrown
* @param numRetries number of previous reattempts
* @return <code>true</code> iff call should be repeated, <code>true</code>
* otherwise
*/
public abstract boolean shouldRetry(Throwable t, int numRetries);
/**
* Should delay for some implementation defined amount of time (to give the
* callee, network, etc. time to recover) given the number of previous
* reattempts. Will be called iff shouldRetry() returned <code>true</code>.
* Good implementations should implement some sort of increased delay
* based on the number of reattempts.
*
* @param numRetries number of previous reattempts
* @param log debug log
*/
public abstract void backOff(int numRetries, Logger log);
/**
* Utility type implemented by those atttempting to make remote method calls
* using this retry mechanism. The call() method should implement the
* desired remote call. Easiest implementation is using an anonymous inner
* class instantiated on the fly, per call (see main example above).
*/
public static abstract class Caller<RetType>
{
/**
* Makes a remote method call which returns a value. Users should change
* the exception signature to be that of the actual method call.
*/
public abstract RetType call()
throws Exception;
}
/**
* Simple subclass of Caller for use by remote method calls which do not
* need to return values. User should implement voidCall() instead of
* call().
*/
public static abstract class VoidCaller extends Caller<Object>
{
@Override
public final Object call()
throws Exception
{
voidCall();
return null;
}
/**
* Makes a remote method call which returns no value. Users should change
* the exception signature to be that of the actual method call.
*/
public abstract void voidCall()
throws Exception;
}
/**
* Simple implementation of RemoteRetry which retries RemoteExceptions some
* number of times and uses the backoff strategy from
* {@link RemoteRetry#simpleBackOff}.
*/
public static class Simple extends RemoteRetry
{
/** default number of times to retry */
public static final int DEFAULT_NUM_MAX_RETRIES = 5;
/** user defined number of times to retry a remote method call which
throws a RemoteException */
private int _maxNumRetries;
public Simple() {
this(DEFAULT_NUM_MAX_RETRIES);
}
public Simple(int maxNumRetries) {
_maxNumRetries = maxNumRetries;
}
public int getMaxNumRetries() {
return _maxNumRetries;
}
@Override
public boolean shouldRetry(Throwable t, int numRetries)
{
if(t instanceof RemoteException) {
return(numRetries < getMaxNumRetries());
}
return false;
}
@Override
public void backOff(int numRetries, Logger log)
{
simpleBackOff(numRetries, log);
}
}
/**
* Simple implementation of RemoteRetry which <b>always</b> retries
* RemoteExceptions thrown from the remote method call. This will make a
* remote method call behave similarly to a local method call in that it
* will not fail for any network related reason (can still fail if callee
* throws a normal exception). This should be used with <b>extreme care</b>
* as it can hang a program which is talking to a dead remote callee, and
* should generally not be used in any sort of robust, enterprise grade
* software (which should always have the ability to handle remote
* failures).
*/
public static abstract class Always extends RemoteRetry
{
protected Always() {}
@Override
public final boolean shouldRetry(Throwable t, int numRetries)
{
return(t instanceof RemoteException);
}
}
/**
* Simple implementation of Always retry strategy which uses the backoff
* strategy from {@link RemoteRetry#simpleBackOff}. Please read warning in
* {@link Always} before using.
*/
public static class SimpleAlways extends Always
{
public SimpleAlways() {}
@Override
public void backOff(int numRetries, Logger log)
{
simpleBackOff(numRetries, log);
}
}
/**
* Simple implementation of RemoteRetry which never retries. This is useful
* for users of utilities which parameterize the RemoteRetry type used by
* the utility where the user does not want any retry attempts.
*/
public static final class Never extends RemoteRetry
{
public Never() {}
@Override
public boolean shouldRetry(Throwable t, int numRetries)
{
return false;
}
@Override
public void backOff(int numRetries, Logger log)
{
throw new UnsupportedOperationException("Should never be called");
}
}
}