package io.fathom.cloud.persist;
import java.lang.reflect.Method;
import javax.persistence.EntityTransaction;
import javax.persistence.OptimisticLockException;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import com.google.inject.persist.UnitOfWork;
/**
* Based on JpaLocalTxnInterceptor
*/
class ZookeeperLocalTxnInterceptor implements MethodInterceptor {
private static final Logger log = LoggerFactory.getLogger(ZookeeperLocalTxnInterceptor.class);
@Inject
private final ZookeeperPersistService emProvider = null;
@Inject
private final UnitOfWork unitOfWork = null;
@Transactional
private static class Internal {
}
// TODO: In Guice, this is ThreadLocal. Not clear why!
// Tracks if the unit of work was begun implicitly by this transaction.
// private final ThreadLocal<Boolean> didWeStartWork = new
// ThreadLocal<Boolean>();
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
int attempt = 0;
int maxAttempts = maxAttempts();
while (true) {
attempt++;
try {
boolean didWeStartWork = false;
// Should we start a unit of work?
if (!emProvider.isWorking()) {
emProvider.begin();
didWeStartWork = true;
}
ZookeeperEntityManager em = this.emProvider.get();
// Allow 'joining' of transactions if there is an enclosing
// @Transactional method.
if (em.getTransaction().isActive()) {
// Don't retry at this level; rely on the outer transaction
maxAttempts = 0;
return methodInvocation.proceed();
}
return invoke0(methodInvocation, em, didWeStartWork);
} catch (OptimisticLockException e) {
boolean retry = false;
if (attempt < maxAttempts) {
retry = true;
}
if (!retry) {
// throw new CloudException(
// "Unable to update due to concurrent modification",
// e);
if (maxAttempts != 0) {
log.warn("Too many retries on OptimisticLockException", e);
}
throw e;
} else {
log.warn("Retrying after OptimisticLockException");
continue;
}
}
}
}
private Object invoke0(MethodInvocation methodInvocation, ZookeeperEntityManager em, boolean didWeStartWork)
throws Throwable, Exception {
Transactional transactional = readTransactionMetadata(methodInvocation);
final EntityTransaction txn = em.getTransaction();
txn.begin();
Object result;
try {
result = methodInvocation.proceed();
} catch (Exception e) {
// commit transaction only if rollback didnt occur
if (rollbackIfNecessary(transactional, e, txn)) {
txn.commit();
}
// propagate whatever exception is thrown anyway
throw e;
} finally {
// Close the em if necessary (guarded so this code doesn't
// run
// unless catch fired).
if (didWeStartWork && !txn.isActive()) {
didWeStartWork = false;
unitOfWork.end();
}
}
// everything was normal so commit the txn (do not move into try
// block
// above as it
// interferes with the advised method's throwing semantics)
try {
txn.commit();
} finally {
// close the em if necessary
if (didWeStartWork) {
didWeStartWork = false;
unitOfWork.end();
}
}
// or return result
return result;
}
private int maxAttempts() {
return 5;
}
// TODO(dhanji): Cache this method's results.
private Transactional readTransactionMetadata(MethodInvocation methodInvocation) {
Transactional transactional;
Method method = methodInvocation.getMethod();
Class<?> targetClass = methodInvocation.getThis().getClass();
transactional = method.getAnnotation(Transactional.class);
if (null == transactional) {
// If none on method, try the class.
transactional = targetClass.getAnnotation(Transactional.class);
}
if (null == transactional) {
// If there is no transactional annotation present, use the default
transactional = Internal.class.getAnnotation(Transactional.class);
}
return transactional;
}
/**
* Returns True if rollback DID NOT HAPPEN (i.e. if commit should continue).
*
* @param transactional
* The metadata annotaiton of the method
* @param e
* The exception to test for rollback
* @param txn
* A JPA Transaction to issue rollbacks on
*/
private boolean rollbackIfNecessary(Transactional transactional, Exception e, EntityTransaction txn) {
boolean commit = true;
if (e instanceof OptimisticLockException) {
txn.rollback();
return false;
}
// check rollback clauses
for (Class<? extends Exception> rollBackOn : transactional.rollbackOn()) {
// if one matched, try to perform a rollback
if (rollBackOn.isInstance(e)) {
commit = false;
// check ignore clauses (supercedes rollback clause)
for (Class<? extends Exception> exceptOn : transactional.ignore()) {
// An exception to the rollback clause was found, DON'T
// rollback
// (i.e. commit and throw anyway)
if (exceptOn.isInstance(e)) {
commit = true;
break;
}
}
// rollback only if nothing matched the ignore check
if (!commit) {
txn.rollback();
}
// otherwise continue to commit
break;
}
}
return commit;
}
}