package com.bagri.server.hazelcast.impl; import static com.bagri.core.api.BagriException.ecTransNoNested; import static com.bagri.core.api.BagriException.ecTransNotFound; import static com.bagri.core.api.BagriException.ecTransWrongState; import static com.bagri.core.server.api.CacheConstants.*; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import javax.management.openmbean.CompositeData; import javax.management.openmbean.TabularData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.bagri.client.hazelcast.impl.IdGeneratorImpl; import com.bagri.core.api.HealthState; import com.bagri.core.api.TransactionIsolation; import com.bagri.core.api.TransactionState; import com.bagri.core.api.BagriException; import com.bagri.core.model.Counter; import com.bagri.core.model.Transaction; import com.bagri.core.server.api.TransactionManagement; import com.bagri.core.system.TriggerAction.Order; import com.bagri.core.system.TriggerAction.Scope; import com.bagri.server.hazelcast.task.doc.DocumentCleaner; import com.bagri.support.idgen.IdGenerator; import com.bagri.support.idgen.SimpleIdGenerator; import com.bagri.support.stats.StatisticsProvider; import com.bagri.support.util.JMXUtils; import com.hazelcast.core.Cluster; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IExecutorService; import com.hazelcast.core.IMap; import com.hazelcast.core.ITopic; import com.hazelcast.core.Member; import com.hazelcast.core.MultiExecutionCallback; import com.hazelcast.query.Predicate; import com.hazelcast.query.Predicates; public class TransactionManagementImpl implements TransactionManagement, StatisticsProvider, MultiExecutionCallback { private static final Logger logger = LoggerFactory.getLogger(TransactionManagementImpl.class); private static final long TX_START = 5L; private ThreadLocal<Long> thTx = new ThreadLocal<Long>() { @Override protected Long initialValue() { return TX_NO; } }; private AtomicLong cntStarted = new AtomicLong(0); private AtomicLong cntCommited = new AtomicLong(0); private AtomicLong cntRolled = new AtomicLong(0); private SchemaRepositoryImpl repo; //private HazelcastInstance hzInstance; private Cluster cluster; private IdGenerator<Long> txGen; private ITopic<Counter> cTopic; private IExecutorService execService; private IMap<Long, Transaction> txCache; private TriggerManagementImpl triggerManager; private long txTimeout = 0; public void setRepository(SchemaRepositoryImpl repo) { this.repo = repo; triggerManager = (TriggerManagementImpl) repo.getTriggerManagement(); setHzInstance(repo.getHzInstance()); } public void setHzInstance(HazelcastInstance hzInstance) { //this.hzInstance = hzInstance; cluster = hzInstance.getCluster(); txCache = hzInstance.getMap(CN_XDM_TRANSACTION); txGen = new IdGeneratorImpl(hzInstance.getAtomicLong(SQN_TRANSACTION)); // not a bottleneck at all! //txGen = new SimpleIdGenerator(); txGen.adjust(TX_START); cTopic = hzInstance.getTopic(TPN_XDM_COUNTERS); execService = hzInstance.getExecutorService(PN_XDM_TRANS_POOL); } public long getTransactionTimeout() { return txTimeout; } public void setTransactionTimeout(long timeout) throws BagriException { this.txTimeout = timeout; } public void adjustTxCounter(long maxUsedId) { Set<Long> ids = new HashSet<>(txCache.localKeySet()); if (maxUsedId > 0) { ids.add(maxUsedId); } if (ids.size() > 0) { Long maxId = Collections.max(ids); boolean adjusted = txGen.adjust(maxId); logger.info("adjustTxCounter; found maxTxId: {}; adjusted: {}", maxId, adjusted); } } @Override public long beginTransaction() throws BagriException { // get default isolation level from some config.. return beginTransaction(TransactionIsolation.readCommited); } @Override public long beginTransaction(TransactionIsolation txIsolation) throws BagriException { logger.trace("beginTransaction.enter; txIsolation: {}", txIsolation); long txId = thTx.get(); if (txId > TX_NO && txCache.containsKey(txId)) { throw new BagriException("nested transactions are not supported; current txId: " + txId, ecTransNoNested); } txId = txGen.next(); // TODO: do this via EntryProcessor? Transaction xTx = new Transaction(txId, cluster.getClusterTime(), 0, repo.getUserName(), txIsolation, TransactionState.started); triggerManager.applyTrigger(xTx, Order.before, Scope.begin); txCache.set(txId, xTx); thTx.set(txId); cntStarted.incrementAndGet(); triggerManager.applyTrigger(xTx, Order.after, Scope.begin); logger.trace("beginTransaction.exit; started tx: {}; returning: {}", xTx, txId); return txId; } @Override public void commitTransaction(long txId) throws BagriException { logger.trace("commitTransaction.enter; got txId: {}", txId); // TODO: do this via EntryProcessor? Transaction xTx = txCache.get(txId); if (xTx != null) { triggerManager.applyTrigger(xTx, Order.before, Scope.commit); xTx.finish(true, cluster.getClusterTime()); //txCache.delete(txId); txCache.set(txId, xTx); } else { throw new BagriException("no transaction found for TXID: " + txId, ecTransNotFound); } thTx.set(TX_NO); cntCommited.incrementAndGet(); triggerManager.applyTrigger(xTx, Order.after, Scope.commit); cTopic.publish(new Counter(true, xTx.getDocsCreated(), xTx.getDocsUpdated(), xTx.getDocsDeleted())); cleanAffectedDocuments(xTx); logger.trace("commitTransaction.exit; tx: {}", xTx); } @Override public void rollbackTransaction(long txId) throws BagriException { logger.trace("rollbackTransaction.enter; got txId: {}", txId); // TODO: do this via EntryProcessor? Transaction xTx = txCache.get(txId); if (xTx != null) { triggerManager.applyTrigger(xTx, Order.before, Scope.rollback); xTx.finish(false, cluster.getClusterTime()); txCache.set(txId, xTx); } else { throw new BagriException("No transaction found for TXID: " + txId, ecTransNotFound); } thTx.set(TX_NO); cntRolled.incrementAndGet(); triggerManager.applyTrigger(xTx, Order.after, Scope.rollback); cTopic.publish(new Counter(false, xTx.getDocsCreated(), xTx.getDocsUpdated(), xTx.getDocsDeleted())); cleanAffectedDocuments(xTx); logger.trace("rollbackTransaction.exit; tx: {}", xTx); } @Override public boolean finishCurrentTransaction(boolean rollback) throws BagriException { long txId = getCurrentTxId(); if (txId > TX_NO) { if (rollback) { rollbackTransaction(txId); } else { commitTransaction(txId); } return true; } return false; } @Override public boolean isInTransaction() { return getCurrentTxId() > TX_NO; } private void cleanAffectedDocuments(Transaction xTx) { // asynchronous cleaning.. //execService.submitToAllMembers(new DocumentCleaner(xTx), this); // synchronous cleaning.. causes a deadlock if used from the common schema exec-pool. // that is why we use separate exec-pool for transaction tasks Map<Member, Future<Transaction>> values = execService.submitToAllMembers(new DocumentCleaner(xTx)); Transaction txClean = null; for (Future<Transaction> value: values.values()) { try { Transaction tx = value.get(); if (txClean == null) { txClean = tx; } else { txClean.updateCounters(tx.getDocsCreated(), tx.getDocsUpdated(), tx.getDocsDeleted()); } } catch (InterruptedException | ExecutionException ex) { logger.error("cleanAffectedDocuments.error;", ex); } } logger.trace("cleanAffectedDocuments; going to complete {}", txClean); completeTransaction(txClean); } boolean isTxVisible(long txId) throws BagriException { if (txId <= TX_INIT) { return true; } long cTx = getCurrentTxId(); if (txId == cTx) { // current tx; return true; } Transaction xTx; TransactionIsolation txIsolation; if (cTx != TX_NO) { // can not be null! xTx = txCache.get(cTx); if (xTx == null) { throw new BagriException("Can not find current Transaction with txId " + cTx + "; txId: " + txId, ecTransNotFound); } // current tx is already finished! if (xTx.getTxState() != TransactionState.started) { throw new BagriException("Current Transaction is already " + xTx.getTxState(), ecTransWrongState); } txIsolation = xTx.getTxIsolation(); if (txIsolation == TransactionIsolation.dirtyRead) { // current tx is dirtyRead, can see not-committed tx results return true; } } else { // default isolation level txIsolation = TransactionIsolation.readCommited; } xTx = txCache.get(txId); boolean commited = xTx == null || xTx.getTxState() == TransactionState.commited; if (txIsolation == TransactionIsolation.readCommited) { return commited; } // txIsolation is repeatableRead or serializable if (txId > cTx) { // the tx started after current, so it is not visible // for current tx return false; } return commited; } long getCurrentTxId() { return thTx.get(); } void flushCurrentTx() throws BagriException { long txId = getCurrentTxId(); if (txId > TX_NO) { rollbackTransaction(txId); } } void updateCounters(int created, int updated, int deleted) throws BagriException { long txId = getCurrentTxId(); if (txId > TX_NO) { Transaction xTx = txCache.get(txId); if (xTx != null) { xTx.updateCounters(created, updated, deleted); txCache.set(txId, xTx); } else { throw new BagriException("no transaction found for TXID: " + txId, ecTransNotFound); } } else { throw new BagriException("not in transaction", ecTransWrongState); } } @Override public <V> V callInTransaction(long txId, boolean readOnly, Callable<V> call) throws BagriException { logger.trace("callInTransaction.enter; got txId: {}", txId); boolean autoCommit = txId == TX_NO; if (autoCommit) { // do not begin tx if it is read-only! if (!readOnly) { txId = beginTransaction(); } } else { thTx.set(txId); } readOnly = txId == TX_NO; try { V result = call.call(); if (autoCommit && !readOnly) { commitTransaction(txId); } logger.trace("callInTransaction.exit; returning: {}", result); return result; } catch (Exception ex) { logger.error("callInTransaction.error; in transaction: " + txId, ex); // even for non autoCommit ?! if (!readOnly) { rollbackTransaction(txId); } if (ex instanceof BagriException) { throw (BagriException) ex; } throw new BagriException(ex, BagriException.ecTransaction); } } @Override public CompositeData getStatisticTotals() { Map<String, Object> result = new HashMap<String, Object>(4); long started = cntStarted.get(); long commited = cntCommited.get(); long rolled = cntRolled.get(); result.put("Started", started); result.put("Commited", commited); result.put("Rolled Back", rolled); result.put("In Progress", started - commited - rolled); return JMXUtils.mapToComposite("Transaction statistics", "Transaction statistics", result); } @Override public TabularData getStatisticSeries() { // return InProgress Transactions here!? Predicate<Long, Transaction> f = Predicates.equal("txState", TransactionState.started); Collection<Transaction> txStarted = txCache.values(f); if (txStarted == null || txStarted.isEmpty()) { return null; } TabularData result = null; String desc = "InProgress Transactions"; String name = "InProgress Transactions"; String header = "txId"; for (Transaction xTx: txStarted) { try { Map<String, Object> txStats = xTx.convert(); //stats.put(header, entry.getKey()); CompositeData data = JMXUtils.mapToComposite(name, desc, txStats); result = JMXUtils.compositeToTabular(name, desc, header, result, data); } catch (Exception ex) { logger.error("getStatisticSeries; error", ex); } } //logger.trace("getStatisticSeries.exit; returning: {}", result); return result; } @Override public void resetStatistics() { cntStarted = new AtomicLong(0); cntCommited = new AtomicLong(0); cntRolled = new AtomicLong(0); } @Override public void onResponse(Member member, Object value) { logger.trace("onResponse; got response: {} from member: {}", value, member); } @Override public void onComplete(Map<Member, Object> values) { logger.trace("onComplete; got values: {}", values); Transaction txClean = null; for (Object value: values.values()) { Transaction tx = (Transaction) value; if (txClean == null) { txClean = tx; } else { txClean.updateCounters(tx.getDocsCreated(), tx.getDocsUpdated(), tx.getDocsDeleted()); } } if (txClean != null) { completeTransaction(txClean); } else { logger.info("onComplete; got empty complete response"); } } private void completeTransaction(Transaction txClean) { Transaction txSource = txCache.get(txClean.getTxId()); if (txSource != null) { if (txSource.getDocsCreated() != txClean.getDocsCreated() || txSource.getDocsUpdated() != txClean.getDocsUpdated() || txSource.getDocsDeleted() != txClean.getDocsDeleted()) { logger.info("completeTransaction; wrong number of cleaned documents; expected: {}, reported: {}", txSource, txClean); } txCache.delete(txClean.getTxId()); } else { logger.info("completeTransaction; got complete response for unknown tx: {}", txClean); } } }