/* * Copyright 2013 NGDATA nv * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lilyproject.client.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.lilyproject.client.NoServersException; import org.lilyproject.client.RetryConf; import org.lilyproject.repository.api.ConcurrentRecordUpdateException; import org.lilyproject.repository.api.IOBlobException; import org.lilyproject.repository.api.IORecordException; import org.lilyproject.repository.api.IOTypeException; import org.lilyproject.repository.api.RetriesExhaustedException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class RetryUtil { private Log log = LogFactory.getLog(getClass()); private RetryConf retryConf; protected RetryUtil(RetryConf retryConf) { this.retryConf = retryConf; } public static <T> T getRetryingInstance(T delegate, Class<T> delegateType, RetryConf retryConf) { RetryUtil retryUtil = new RetryUtil(retryConf); InvocationHandler ih = new RetryingInvocationHandler<T>(delegate, retryUtil); T retryingInstance = (T)Proxy.newProxyInstance(RetryUtil.class.getClassLoader(), new Class[]{delegateType}, ih); return retryingInstance; } private static final class RetryingInvocationHandler<T> implements InvocationHandler { private final T delegate; private final RetryUtil retryUtil; private RetryingInvocationHandler(T delegate, RetryUtil retryUtil) { this.delegate = delegate; this.retryUtil = retryUtil; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("close")) { return null; } return retryUtil.retry(delegate, method, args); } } public Object retry(Object delegate, Method method, Object[] args) throws Throwable { long startedAt = System.currentTimeMillis(); int attempt = 0; while (true) { try { return method.invoke(delegate, args); } catch (Throwable throwable) { handleThrowable(throwable, method, startedAt, attempt); } attempt++; } } public void handleThrowable(Throwable throwable, Method method, long startedAt, int attempt) throws Throwable { if (throwable instanceof InvocationTargetException) { throwable = ((InvocationTargetException)throwable).getTargetException(); } if (throwable instanceof InterruptedException) { throw throwable; } if (throwable instanceof IORecordException || throwable instanceof IOBlobException || throwable instanceof IOTypeException || throwable instanceof ConcurrentRecordUpdateException || throwable instanceof NoServersException) { boolean callInitiated = true; if (throwable.getCause() instanceof NoServersException) { // I initially thought we could also assume the request was not yet launched in case of // ConnectException with msg "Connection refused". However, at least with the Avro HttpTransceiver, // this exception can also occur when the connection is lost between writing the request // and reading the response. On reading the response, the Java URLConnection will see // there is no connection anymore and reestablish it, hence giving a "connection refused" error. // In this situation, the request is sent out by the server, so it is not safe to simply redo it. callInitiated = false; } handleRetry(method, startedAt, attempt, callInitiated, throwable); } else { throw throwable; } } public void handleRetry(Method method, long startedAt, int attempt, boolean callInitiated, Throwable throwable) throws Throwable { long timeSpentRetrying = System.currentTimeMillis() - startedAt; if (timeSpentRetrying > retryConf.getRetryMaxTime()) { throw new RetriesExhaustedException(getOpString(method), attempt, timeSpentRetrying, throwable); } String methodName = method.getName(); boolean retry = false; // Since the "newSomething" methods are simple factory methods, put them in the same class as reads // TODO: the methods starting with get include the blob methods getInputStream and getOutputStream, // which should probably have a different treatment if ((methodName.startsWith("read") || methodName.startsWith("get") || methodName.startsWith("new")) && retryConf.getRetryReads()) { retry = true; } else if (methodName.equals("createOrUpdate") && retryConf.getRetryCreateOrUpdate()) { retry = true; } else if (methodName.startsWith("update") && retryConf.getRetryUpdates()) { retry = true; } else if (methodName.startsWith("delete") && retryConf.getRetryDeletes()) { retry = true; } else if (methodName.startsWith("create") && retryConf.getRetryCreate() && (!callInitiated || retryConf.getRetryCreateRiskDoubles())) { retry = true; } if (retry) { int sleepTime = getSleepTime(attempt); if (log.isDebugEnabled() || log.isInfoEnabled()) { String message = "Sleeping " + sleepTime + "ms before retrying operation " + getOpString(method) + " attempt " + attempt + " failed due to " + throwable.toString(); if (log.isDebugEnabled()) { log.debug(message, throwable); } else if (log.isInfoEnabled()) { log.info(message); } } Thread.sleep(sleepTime); } else { throw throwable; } } private int getSleepTime(int attempt) throws InterruptedException { int pos = attempt < retryConf.getRetryIntervals().length ? attempt : retryConf.getRetryIntervals().length - 1; return retryConf.getRetryIntervals()[pos]; } private String getOpString(Method method) { return method.getDeclaringClass().getSimpleName() + "." + method.getName(); } }