package com.maxifier.guice.jpa; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.PersistenceException; import java.lang.reflect.Method; import java.sql.SQLTransientException; import java.util.Random; import static com.maxifier.guice.jpa.DB.Transaction.NOT_REQUIRED; import static com.maxifier.guice.jpa.DB.Transaction.REQUIRES_NEW; /** * Intercepts methods marked by {@link DB @DB} and initialize database-enabled context. * <p>Database context held by {@link UnitOfWork} in thread local variable.</p> * <p>Use {@link DBEntityManagerProvider} to obtain context-sensitive {@code EntityManager} instances.</p> * * @author Konstantin Lyamshin (2015-11-15 23:25) */ public class DBInterceptor implements MethodInterceptor { private static final Logger logger = LoggerFactory.getLogger(DBInterceptor.class); private static final Random RND = new Random(); private static final int RETRY_TIMEOUTS[] = new int[]{ 0, 307, 2000, 11000, 19000, 31000, 53000, 89000, 151000, 241000, 307000 }; @Override public Object invoke(MethodInvocation invocation) throws Throwable { // I don't care bridge methods so much because nested invocations cost almost nothing. // Significant resources spend on DB connect only, but in case of bridge methods connection // obtained only in the most deep invocation. In this case outer invocation became // connection and/or transaction owner and closes it properly. // In case of REQUIRES_NEW nested invocations create extra UnitOfWork, but // connection obtained only in the most deep one and almost no extra resources wasted. Method method = invocation.getMethod(); DB config = method.getAnnotation(DB.class); if (config == null) { throw new IllegalStateException("@DB annotation not found on " + method); } // method retry processing int retries = 0; while (true) { try { return invoke0(config.transaction(), invocation); } catch (PersistenceException e) { if (e.getCause() instanceof SQLTransientException && retries++ < config.retries()) { if (delayRetry(retries)) { logger.info(String.format("Retry #%d of %s", retries, method), e); continue; } } throw e; } } } private static boolean delayRetry(int n) { int maxN = RETRY_TIMEOUTS.length - 1; int timeout = n < maxN ? RETRY_TIMEOUTS[n] : RETRY_TIMEOUTS[maxN - 1] + RND.nextInt(RETRY_TIMEOUTS[maxN]); try { Thread.sleep(timeout); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); return false; } return true; } private Object invoke0(DB.Transaction transaction, MethodInvocation invocation) throws Throwable { UnitOfWork context = UnitOfWork.get(); boolean connectionOwner = transaction == REQUIRES_NEW || context == null; if (connectionOwner) { context = UnitOfWork.create(); } try { boolean transactionOwner = transaction != NOT_REQUIRED && context.startTransaction(); try { return invocation.proceed(); } catch (Exception e) { context.setRollbackOnly(); throw e; } finally { if (transactionOwner) { context.endTransaction(); } } } finally { if (connectionOwner) { context.releaseConnection(); } } } }