package com.smartitengineering.cms.repo.dao.impl.tx; import com.google.inject.Inject; import com.google.inject.Singleton; import com.smartitengineering.cms.repo.dao.impl.AbstractRepositoryDomain; import com.smartitengineering.cms.repo.dao.tx.TransactionException; import com.smartitengineering.dao.common.CommonReadDao; import com.smartitengineering.dao.common.CommonWriteDao; import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author imyousuf */ @Singleton public class TransactionServiceImpl implements TransactionService { private final AtomicLong uniqueIdGen = new AtomicLong(Long.MIN_VALUE); private final TransactionInMemoryCache memCache; private final TransactionFactory factory; private final ConcurrentMap<String, Pair<CommonWriteDao<? extends AbstractRepositoryDomain>, CommonReadDao<? extends AbstractRepositoryDomain, String>>> daoCache; private final ConcurrentMap<String, AtomicInteger> opsCounter; private static final Logger LOGGER = LoggerFactory.getLogger(TransactionServiceImpl.class); @Inject public TransactionServiceImpl(TransactionInMemoryCache memCache, TransactionFactory factory) { this.memCache = memCache; this.factory = factory; this.daoCache = new ConcurrentHashMap<String, Pair<CommonWriteDao<? extends AbstractRepositoryDomain>, CommonReadDao<? extends AbstractRepositoryDomain, String>>>(); this.opsCounter = new ConcurrentHashMap<String, AtomicInteger>(); } public String getNextTransactionId() { return String.valueOf(uniqueIdGen.incrementAndGet()); } public void commit(String txId) { if (StringUtils.isBlank(txId)) { return; } List<Pair<TransactionStoreKey, TransactionStoreValue>> list = memCache.getTransactionParticipants(txId); if (list == null || list.isEmpty()) { return; } //Sort Collections.sort(list, new Comparator<Pair<TransactionStoreKey, TransactionStoreValue>>() { public int compare(Pair<TransactionStoreKey, TransactionStoreValue> o1, Pair<TransactionStoreKey, TransactionStoreValue> o2) { return o1.getValue().getOpSequence() - o2.getValue().getOpSequence(); } }); //Commit Deque<Pair<TransactionStoreKey, TransactionStoreValue>> callStack = new LinkedList<Pair<TransactionStoreKey, TransactionStoreValue>>(); for (Pair<TransactionStoreKey, TransactionStoreValue> val : list) { TransactionStoreKey key = val.getKey(); TransactionStoreValue value = val.getValue(); try { final CommonWriteDao<? extends AbstractRepositoryDomain> writeDao = daoCache.get(key.getObjectType().getName()). getKey(); OpState opState = value.getOpState(); if (opState == null) { callStack.push(val); continue; } switch (opState) { case SAVE: writeDao.save(value.getCurrentState()); break; case UPDATE: writeDao.update(value.getCurrentState()); break; case DELETE: writeDao.delete(value.getCurrentState()); break; default: } callStack.push(val); } catch (Exception ex) { LOGGER.warn("Exception trying to perform commit. Go for hard rollback", ex); rollback(callStack); throw new TransactionException(ex); } finally { completeTx(txId); } } } public void rollback(String txId) { completeTx(txId); } public <T extends AbstractRepositoryDomain> void save(TransactionElement<T> element) { String id = (String) element.getDto().getId(); if (id == null) { id = UUID.randomUUID().toString(); element.getDto().setId(id); } populateCache(element, OpState.SAVE); } public <T extends AbstractRepositoryDomain> void update(TransactionElement<T> element) { populateCache(element, OpState.UPDATE); } public <T extends AbstractRepositoryDomain> void delete(TransactionElement<T> element) { populateCache(element, OpState.DELETE); } /** * Cleanup all references and records of transaction id in the service implementation. * @param txId The transaction to complete */ protected void completeTx(String txId) { opsCounter.remove(txId); } /** * A hard rollback implementation, that will actually undo what has been done. This will specially occur in case of * commit if the commit causes some error in the DAO implementation; e.g., validation error. * @param opsPerformed The stack of operations to revert. */ protected void rollback(Deque<Pair<TransactionStoreKey, TransactionStoreValue>> opsPerformed) { if (opsPerformed == null || opsPerformed.isEmpty()) { return; } Pair<TransactionStoreKey, TransactionStoreValue> val = opsPerformed.pop(); do { TransactionStoreKey key = val.getKey(); TransactionStoreValue value = val.getValue(); if (opsPerformed.isEmpty()) { val = null; } else { val = opsPerformed.pop(); } try { final CommonWriteDao<? extends AbstractRepositoryDomain> writeDao = daoCache.get(key.getObjectType().getName()). getKey(); OpState opState = value.getOpState(); if (opState != null) { opState = getHardRollbackState(opState); } else { continue; } switch (opState) { case SAVE: writeDao.save(value.getCurrentState()); break; case UPDATE: writeDao.update(value.getCurrentState()); break; case DELETE: writeDao.delete(value.getCurrentState()); break; default: } } catch (Exception ex) { LOGGER.warn("Exception trying to perform a hard rollback. Ignoring and continuing", ex); } } while (val != null); } /** * Populate cache by either updating it or inserting it. Also in process implement state transition of the cached * object. * @param <T> The type of domain * @param element The transaction element to cache * @param currentOpState The current operatoin state */ protected <T extends AbstractRepositoryDomain> void populateCache(TransactionElement<T> element, OpState currentOpState) { fillCacheIfNeeded(element); int nextOpIndex = getNextOpsIndex(element.getTxId()); TransactionStoreKey key = factory.createTransactionStoreKey(); key.setOpTimestamp(System.currentTimeMillis()); key.setTransactionId(element.getTxId()); key.setObjectType(element.getDto().getClass()); key.setObjectId((String) element.getDto().getId()); Pair<TransactionStoreKey, TransactionStoreValue> pair = memCache.getValueForIsolatedTransaction(key); if (pair == null) { TransactionStoreValue value = factory.createTransactionStoreValue(); value.setCurrentState(element.getDto()); value.setOpSequence(nextOpIndex); value.setOpState(currentOpState); if (!OpState.SAVE.equals(currentOpState)) { value.setOriginalState(element.getReadDao().getById(key.getObjectId())); } memCache.storeTransactionValue(key, value); } else { TransactionStoreValue value = pair.getValue(); value.setCurrentState(element.getDto()); value.setOpState(getActualStateAfterTransition(value.getOpState(), currentOpState)); } } /** * Implements state transition and retrieves actual state relative to earlier state and current state. * @param oldOpState The old state * @param currentOpState The current op state * @return The actual state, IOW, the Operation to attain when written to repository. Null means the transaction op * can be ignored. */ protected OpState getActualStateAfterTransition(OpState oldOpState, OpState currentOpState) { if (oldOpState == null) { if (currentOpState == null) { return null; } else { switch (currentOpState) { case UPDATE: case DELETE: throw new IllegalStateException("Can not update/delete a bean that does not exist"); case SAVE: return OpState.SAVE; } } } else { if (currentOpState == null) { throw new IllegalStateException("Can not update/delete a bean that does not exist"); } else { switch (oldOpState) { case SAVE: { switch (currentOpState) { case SAVE: throw new IllegalStateException( "Constraint violation exception, a object with same id has already been saved"); case DELETE: return null; case UPDATE: default: return oldOpState; } } case UPDATE: { switch (currentOpState) { case SAVE: throw new IllegalStateException("Can not save an existing bean"); case DELETE: return currentOpState; case UPDATE: default: return oldOpState; } } case DELETE: { switch (currentOpState) { case SAVE: return OpState.UPDATE; case UPDATE: case DELETE: default: throw new IllegalStateException( "Can not update/delete a bean that has been deleted earlier in the transaction"); } } } } } return currentOpState; } private OpState getHardRollbackState(OpState actualState) { switch (actualState) { case SAVE: return OpState.DELETE; case DELETE: return OpState.SAVE; case UPDATE: return OpState.UPDATE; default: return null; } } private <T extends AbstractRepositoryDomain> void fillCacheIfNeeded(TransactionElement<T> element) { daoCache.putIfAbsent(element.getObjectType().getName(), new Pair<CommonWriteDao<? extends AbstractRepositoryDomain>, CommonReadDao<? extends AbstractRepositoryDomain, String>>( element.getWriteDao(), element.getReadDao())); opsCounter.putIfAbsent(element.getTxId(), new AtomicInteger(-1)); } private int getNextOpsIndex(String txId) { return opsCounter.get(txId).incrementAndGet(); } }