package org.gbif.occurrence.processor.interpreting.util; import java.net.URI; import java.net.URL; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.sun.jersey.api.client.GenericType; import com.sun.jersey.api.client.WebResource; /** * A cache loader that will call a webservice, falling back into a retry mechanism should any * errors occur in the web service call. Developers are urged to consider carefully their use * before adopting this approach. This is particularly suitable for operations such as batch * processing, where one knows the client will be making a large number of identical web service * calls in a short space of time, and changes during that time are not of high importance. * Note that {@link WebResource} is suitable as the key * since it uses {@link URI} and not {@link URL} which the JVM does not try to resolve at runtime. * Construction is forced through static factories to ensure that generics are handled correctly, * removing the need for casting at use. * The following illustrates the intended use of this class: * <pre> * LoadingCache<WebResource, NameUsageMatch> cache = CacheBuilder.newBuilder() * .maximumSize(1000) * .expireAfterAccess(1, TimeUnit.MINUTES) * .build(RetryingWebserviceClient.newInstance(NameUsageMatch.class, NUM_RETRIES, RETRY_PERIOD_MSEC)); * NameUsageMatch lookup = cache.get(RESOURCE.queryParams(queryParams)); * </pre> * * TODO: this is a copy from occurrence-store - move to somewhere common */ public class RetryingWebserviceClient<T> extends CacheLoader<WebResource, T> { private static final Logger LOGGER = LoggerFactory.getLogger(RetryingWebserviceClient.class); private final Class<T> type; private final GenericType<T> genericType; private final int numberOfAttempts; private final int retryDelayMsec; public static final int DEFAULT_NUMBER_OF_ATTEMPTS = 1; public static final int DEFAULT_DELAY_MSECS = 0; /** * Creates a new instance using the default configuration {@link #DEFAULT_NUMBER_OF_ATTEMPTS} * and {@link #DEFAULT_DELAY_MSECS}. * * @param type Of resource to be returned by the web service * * @return A cacheloader suitable for using in a Google {@link CacheBuilder} */ public static <T> RetryingWebserviceClient<T> newInstance(Class<T> type) { return new RetryingWebserviceClient<T>(type, DEFAULT_NUMBER_OF_ATTEMPTS, DEFAULT_DELAY_MSECS); } /** * Creates a new instance using the provided configuration. * * @param type Of resource to be returned by the web service * @param numberOfAttempts Which must be 1 or more * @param retryDelayMsec Which must be 0 or more * * @return A cacheloader suitable for using in a Google {@link CacheBuilder} */ public static <T> RetryingWebserviceClient<T> newInstance(Class<T> type, int numberOfAttempts, int retryDelayMsec) { return new RetryingWebserviceClient<T>(type, numberOfAttempts, retryDelayMsec); } /** * Creates a new instance using the default configuration {@link #DEFAULT_NUMBER_OF_ATTEMPTS} * and {@link #DEFAULT_DELAY_MSECS}. * This allows implementation to use the likes of: * <pre> * new GenericType<PagingResponse<Dataset>>() {}; * </pre> * * @param genericType Of the response object * * @return A cacheloader suitable for using in a Google {@link CacheBuilder} */ public static <T> RetryingWebserviceClient<T> newInstance(GenericType<T> genericType) { return new RetryingWebserviceClient<T>(genericType, DEFAULT_NUMBER_OF_ATTEMPTS, DEFAULT_DELAY_MSECS); } /** * Creates a new instance using the provided configuratio, which supports the generic typing of the response object. * This allows implementation to use the likes of: * <pre> * new GenericType<PagingResponse<Dataset>>() {}; * </pre> * * @param genericType Of the response object * @param numberOfAttempts Which must be 1 or more * @param retryDelayMsec Which must be 0 or more * * @return A cacheloader suitable for using in a Google {@link CacheBuilder} */ public static <T> RetryingWebserviceClient<T> newInstance(GenericType<T> genericType, int numberOfAttempts, int retryDelayMsec) { return new RetryingWebserviceClient<T>(genericType, numberOfAttempts, retryDelayMsec); } /** * Force the static factory, to ensure generics are respected. */ private RetryingWebserviceClient(Class<T> type, int numberOfAttempts, int retryDelayMsec) { Preconditions.checkArgument(numberOfAttempts > 0, "Number of attempts must be a positive number"); Preconditions.checkArgument(retryDelayMsec >= 0, "Retry delay must be 0 or more milliseconds"); this.type = Preconditions.checkNotNull(type); this.genericType = null; this.numberOfAttempts = numberOfAttempts; this.retryDelayMsec = retryDelayMsec; } /** * Force the static factory, to ensure generics are respected. */ private RetryingWebserviceClient(GenericType<T> genericType, int numberOfAttempts, int retryDelayMsec) { Preconditions.checkArgument(numberOfAttempts > 0, "Number of attempts must be a positive number"); Preconditions.checkArgument(retryDelayMsec >= 0, "Retry delay must be 0 or more milliseconds"); this.type = null; this.genericType = Preconditions.checkNotNull(genericType); this.numberOfAttempts = numberOfAttempts; this.retryDelayMsec = retryDelayMsec; } @Override public T load(WebResource resource) throws Exception { for (int attempt = 1; attempt <= numberOfAttempts; attempt++) { try { if (type != null) { return resource.get(type); } else { return resource.get(genericType); } } catch (Exception e) { LOGGER.debug("Error looking up resource [{}] in attempt[{}] of max[{}]", resource, attempt, numberOfAttempts, e); if (attempt >= numberOfAttempts) { LOGGER.error("Error looking up resource [{}] in last attempt of [{}]", resource, numberOfAttempts, e); throw e; } if (retryDelayMsec > 0) { TimeUnit.MILLISECONDS.sleep(retryDelayMsec); } } } // Should not ever happen as we propagate the underlying exception above throw new IllegalStateException("Retry count exhausted"); } }