/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.service.components;
import org.apache.log4j.Logger;
import org.dspace.importer.external.exception.MetadataSourceException;
import org.dspace.importer.external.exception.SourceExceptionHandler;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class contains primitives to handle request timeouts and to retry requests.
* This is achieved by classifying exceptions as fatal or as non fatal/retryable.
* Evidently only subclasses can make the proper determination of what is retryable and what isn't.
* This is useful in case the service employs throttling and to deal with general network issues.
* @author Roeland Dillen (roeland at atmire dot com)
* @author Antoine Snyers (antoine at atmire dot com)
*/
public abstract class AbstractRemoteMetadataSource {
protected long lastRequest = 0;
protected long interRequestTime;
protected ReentrantLock lock = new ReentrantLock();
protected int maxRetry = 20;
protected int retry;
protected String operationId;
protected String warning;
protected Map<Class, List<SourceExceptionHandler>> exceptionHandlersMap;
protected Exception error;
/**
* Constructs an empty MetadataSource class object and initializes the Exceptionhandlers
*/
protected AbstractRemoteMetadataSource() {
initExceptionHandlers();
}
/**
* initialize the exceptionHandlersMap with an empty {@link java.util.LinkedHashMap}
*/
protected void initExceptionHandlers() {
exceptionHandlersMap = new LinkedHashMap<>();
// if an exception is thrown that is not in there, it is not recoverable and the retry chain will stop
// by default all exceptions are fatal, but subclasses can add their own handlers for their own exceptions
}
/**
* Return the warning message used for logging during exception catching
*
* @return a "warning" String
*/
public String getWarning() {
return warning;
}
/**
* Set the warning message used for logging
*
* @param warning
* warning message
*/
public void setWarning(String warning) {
this.warning = warning;
}
/**
* Return the number of retries that have currently been undertaken
*
* @return the number of retries
*/
public int getRetry() {
return retry;
}
/**
* Return the number of max retries that can be undertaken before separate functionality kicks in
*
* @return maximum number of retries
*/
public int getMaxRetry() {
return maxRetry;
}
/**
* Set the number of maximum retries before throwing on the exception
*
* @param maxRetry
* maximum number of retries
*/
@Resource(name="maxRetry")
public void setMaxRetry(int maxRetry) {
this.maxRetry = maxRetry;
}
/**
* Retrieve the operationId
*
* @return A randomly generated UUID. generated during the retry method
*/
public String getOperationId() {
return operationId;
}
/**
* Retrieve the last encountered exception
*
* @return An Exception object, the last one encountered in the retry method
*/
public Exception getError() {
return error;
}
/**
* Set the last encountered error
*
* @param error exception to set
*/
public void setError(Exception error) {
this.error = error;
}
/**
* log4j logger
*/
private static Logger log = Logger.getLogger(AbstractRemoteMetadataSource.class);
/**
* Command pattern implementation. the callable.call method will be retried
* until it either succeeds or reaches the try limit. Maybe this should have
* a backoff algorithm instead of waiting a fixed time.
*
* @param callable the callable to call. See the classes with the same name as
* the public methods of this class.
* @param <T> return type. Generics for type safety.
* @return The result of the call
* @throws org.dspace.importer.external.exception.MetadataSourceException if something unrecoverable happens (e.g. network failures)
*/
protected <T> T retry(Callable<T> callable) throws MetadataSourceException {
retry = 0;
operationId = UUID.randomUUID().toString();
while (true) {
try {
lock.lock();
this.error = null;
long time = System.currentTimeMillis() - lastRequest;
if ((time) < interRequestTime) {
Thread.sleep(interRequestTime - time);
}
try {
init();
} catch (Exception e) {
throwSourceException(retry, e, operationId);
}
log.info("operation " + operationId + " started");
T response = callable.call();
log.info("operation " + operationId + " successful");
return response;
} catch (Exception e) {
this.error = e;
if (retry > maxRetry) {
throwSourceException(retry, e, operationId);
}
handleException(retry, e, operationId);
// No MetadataSourceException has interrupted the loop
retry++;
log.warn("Error in trying operation " + operationId + " " + retry + " " + warning + ", retrying !", e);
} finally {
lock.unlock();
}
try{
Thread.sleep(1000L);
} catch (InterruptedException e) {
throwSourceException(retry, e, operationId);
}
}
}
/**
* Handles a given exception or throws on a {@link org.dspace.importer.external.exception.MetadataSourceException} if no ExceptionHandler is set
*
* @param retry The number of retries before the exception was thrown on
* @param exception The exception to handle
* @param operationId The id of the operation that threw the exception
* @throws MetadataSourceException if no ExceptionHandler is configured for the given exception
*/
protected void handleException(int retry, Exception exception, String operationId) throws MetadataSourceException {
List<SourceExceptionHandler> exceptionHandlers = getExceptionHandler(exception);
if (exceptionHandlers != null && !exceptionHandlers.isEmpty()) {
for (SourceExceptionHandler exceptionHandler : exceptionHandlers) {
exceptionHandler.handle(this);
}
} else {
throwSourceException(retry, exception, operationId);
}
}
/**
* Retrieve a list of SourceExceptionHandler objects that have an instanceof the exception configured to them.
*
* @param exception The exception to base the retrieval of {@link org.dspace.importer.external.exception.SourceExceptionHandler} on
* @return a list of {@link org.dspace.importer.external.exception.SourceExceptionHandler} objects
*/
protected List<SourceExceptionHandler> getExceptionHandler(Exception exception) {
for (Class aClass : exceptionHandlersMap.keySet()) {
if (aClass.isInstance(exception)) {
return exceptionHandlersMap.get(aClass);
}
}
return null;
}
/**
* Throw a {@link MetadataSourceException}
*
* @param retry The number of retries before the exception was thrown on
* @param exception The exception to throw
* @param operationId The id of the operation that threw the exception
* @throws MetadataSourceException if no ExceptionHandler is configured for the given exception
*/
protected void throwSourceException(int retry, Exception exception, String operationId) throws MetadataSourceException {
throwSourceExceptionHook();
log.error("Source exception " + exception.getMessage(),exception);
throw new MetadataSourceException("At retry of operation " + operationId + " " + retry, exception);
}
/**
* A specified point where methods can be specified or callbacks can be executed
*/
protected void throwSourceExceptionHook() {
}
/**
* Attempts to init a session
*
* @throws Exception on generic exception
*/
public abstract void init() throws Exception;
}