/******************************************************************************* * Copyright 2014 Miami-Dade County * * 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.sharegov.cirm; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.sharegov.cirm.rdb.CirmTransactionException; import org.sharegov.cirm.rdb.RetryDetectedException; /** * Thread safe thread local Transaction class.<br> * <br> * Initial state: NOTEXECUTED<br> * State transitions:<br> * NOTEXECUTED --> EXECUTING<br> * EXECUTING --> EXECUTING_REQUESTING_RETRY | SUCCEEDED | FAILED<br> * EXECUTING_REQUESTING_RETRY --> EXECUTING | SUCCEEDED | FAILED<br> * <br> * @author Thomas Hilpold * @param <V> * */ public abstract class CirmTransaction<V> implements Callable<V> { public enum STATE { NOTEXECUTED, EXECUTING, EXECUTING_REQUESTING_RETRY, SUCCEEDED, FAILED } /** * Threadlocal association of transaction with executing thread during begin() and end(). * Only the toplevel transaction will be associated with each thread. */ private static final ThreadLocal<CirmTransaction<?>> transactions = new ThreadLocal<CirmTransaction<?>>(); private static final AtomicLong nrOfTotalTransactions = new AtomicLong(0); private static final AtomicLong nrOfFailedTransactions = new AtomicLong(0); /** * Map from ThreadID to CirmTransaction of currently executing (begin to end) top level transactions */ private static final ConcurrentHashMap<Long, CirmTransaction<?>> executingTransactions = new ConcurrentHashMap<Long, CirmTransaction<?>>(); private volatile STATE transactionState = STATE.NOTEXECUTED; private final AtomicInteger executionCount = new AtomicInteger(0); private final AtomicLong beginTimeMs = new AtomicLong(0); private final AtomicLong endTimeMs = new AtomicLong(0); private final AtomicLong executeStartTimeMs = new AtomicLong(0); // set at each retry private final AtomicLong executeEndTimeMs = new AtomicLong(0); // set at each retry or first execution finish private final CirmTransactionEventSupport transactionEventSupport = new CirmTransactionEventSupport(); /** * only during execution, single thread access, lazy initialized */ private UUID transactionUUID = null; private volatile boolean isAllowDBLock = false; // // Transaction Stats and Management // public static long getNrOfTotalTransactions() { return nrOfTotalTransactions.get(); } public static long getNrOfFailedTransactions() { return nrOfFailedTransactions.get(); } public static int getNrOfExecutingTransactions() { return executingTransactions.size(); } /** * Gets all currently executing transactions by their threads' ids. * Do not assume these threads to exist or that transactions are still executing, * when you iterate over the map, as some might be finishing while you are iterating. * * @return a copied map of threadIds to currently executing transactions */ public static synchronized Map<Long, CirmTransaction<?>> getExecutingTransactions() { Map<Long, CirmTransaction<?>> m = new HashMap<Long, CirmTransaction<?>>(executingTransactions); return m; } // // Transaction retrieval for executing thread // /** * To be called by the executing thread (between begin and end) to retrieve the transaction bound to it. * * @return the transaction bound to the current thread */ public static CirmTransaction<?> get() { return transactions.get(); } /** * * @return */ public static boolean isExecutingOnThisThread() { return transactions.get() != null; } /** * A random UUID associated with the toplevel transaction valid during execution to be accessed by the executing thread only. * UUID will remain the same across retries. * Not thread safe! * @return */ public static UUID getTopLevelTransactionUUID() { CirmTransaction<?> topLevel = get(); if (topLevel == null) throw new IllegalStateException("Toplevel transaction is not executing. You must call this method during execution."); return topLevel.getTransactionUUID(); } private UUID getTransactionUUID() { if (transactionUUID == null) transactionUUID = UUID.randomUUID(); return transactionUUID; } private void clearTransactionUUID() { transactionUUID = null; } // // Transaction control // /** * Begin must be called by the transaction controller in the executing thread before the (first) execute call. * It must not be called before retrying execute. * Binds the transaction to the thread during the execution */ public final void begin() { if (isExecutingOnThisThread()) throw new IllegalStateException("A Transaction is already executed by this Thread " + Thread.currentThread().getName()); transactions.set(this); nrOfTotalTransactions.incrementAndGet(); executingTransactions.putIfAbsent(Thread.currentThread().getId(), this); beginTimeMs.set(System.currentTimeMillis()); transactionState = STATE.EXECUTING; } /** * Executes the transaction code in overwritten call method. * Statistical information is also updated. * This method will be called repeatedly after one begin and before one end call, if repeatable exceptions occur during it's run. * Usage: Call this method instead of overwritten call(). * Thread safe. * * @return the result of call() * @throws Exception any exception call() throws */ public final V execute() throws Exception { if (transactions.get() != this) throw new IllegalStateException("Cannot execute transaction not bound to current thread: " + Thread.currentThread().getName()); clearTopLevelEventListeners(); //Reset Retry! transactionState = STATE.EXECUTING; executeStartTimeMs.set(System.currentTimeMillis()); executionCount.incrementAndGet(); V result; try { result = call(); checkRetryRequested(); } finally { executeEndTimeMs.set(System.currentTimeMillis()); } return result; } /** * Set the retryFlag during execution to indicate that a toplevel transaction retry should be attempted immediately and * all subtransaction execution ceased. * This flag will be cleared automatically before each retry execution. * * @param retryFlag * @throws CirmTransactionException */ public void requestRetry() { if (transactionState == STATE.EXECUTING_REQUESTING_RETRY) return; if (transactionState == STATE.NOTEXECUTED) throw new IllegalStateException("Retry requested before Transaction execution began."); if (transactionState == STATE.FAILED) throw new IllegalStateException("Retry requested after Transaction ended with failure."); if (transactionState == STATE.SUCCEEDED) throw new IllegalStateException("Retry requested after Transaction ended successfully."); transactionState = STATE.EXECUTING_REQUESTING_RETRY; } /** * Checks if retry flag is set and interrupts execution by throwing a RetryDetectedException. * * @throws RetryDetectedException if retry flag is set. */ public void checkRetryRequested() throws RetryDetectedException { if (transactionState == STATE.EXECUTING_REQUESTING_RETRY) throw new RetryDetectedException("Retry flag is set. Cease execution and retry toplevel transaction instantly."); } /** * True between calling trans begin and end. * @return */ public boolean isExecuting() { return transactionState == STATE.EXECUTING || transactionState == STATE.EXECUTING_REQUESTING_RETRY ; } /** * True if the transaction is executing and requesting a retry due to a retryable exception. * True indicates that all sublevel transaction execution shall cease * and a toplevel retry should be started immediately. * * @return */ public boolean isRequestingRetry() { return transactionState == STATE.EXECUTING_REQUESTING_RETRY; } /** * End transaction sucessfully. */ public final void end() { end(true); } /** * End must be called by the transaction controller in the executing thread after the transaction failed or succeeded after execution (including retries if any). * It disassociates the transaction from the current thread. * @param succeeded false if transaction failed (timeout, non retryable transaction, retry maximum reached) * @throws CirmTransactionException */ public final void end(boolean succeeded) { if (transactionState == STATE.SUCCEEDED || transactionState == STATE.FAILED) throw new IllegalStateException("Transaction has completed. Duplicate end call."); if (transactionState == STATE.NOTEXECUTED) throw new IllegalStateException("Transaction execution has not yet begun. Cannot end. Call begin first."); if (transactionState == STATE.EXECUTING_REQUESTING_RETRY) System.err.println("Transaction requesting to be retried, but was ended."); if (!succeeded) nrOfFailedTransactions.incrementAndGet(); transactionState = succeeded ? STATE.SUCCEEDED: STATE.FAILED; executingTransactions.remove(Thread.currentThread().getId()); endTimeMs.set(System.currentTimeMillis()); clearTransactionUUID(); transactions.remove(); } public STATE getState() { return transactionState; } public int getExecutionCount() { return executionCount.get(); } /** * The transaction has ended execution including all retries. * * @return */ public boolean isEnded() { return transactionState == STATE.SUCCEEDED || transactionState == STATE.FAILED; } /** * The transaction has failed including all retries. * * @return */ public boolean isFailed() { return transactionState == STATE.FAILED; } /** * The transaction has failed including all retries. * * @return */ public boolean isSucceeded() { return transactionState == STATE.SUCCEEDED; } // // DB locking // /** * If the transaction controller sets AllowDBLock earlier, a transaction may acquire DB locks to prevent it * from being retried again. Only the store should use this method to acquire locks. * @return */ public boolean isAllowDBLock() { return isAllowDBLock; } /** * This method shall only be called by the Transaction Controller. * Thread safe. * @param isAllowDBLock */ public void setAllowDBLock(boolean isAllowDBLock) { this.isAllowDBLock = isAllowDBLock; } // // Transaction timing // /** * Begin time of the transaction. Equal across all retries. * * Valid only in the toplevel transaction after txn controller has begun transaction, during execution and after finish. * Zero will be returned before beginning of transaction. * Thread safe. * @return a long value as defined by System.currentTimeMillis() at the time the transaction began. */ public long getBeginTimeMs() { return beginTimeMs.get(); } /** * Returns the duration between begin call and end call for completed (succeeded or failed) transactions * or (begin call and currrent time, if transaction is still executing) * or -1 if begin has not been called. * * @return */ public double getTotalExecutionTimeSecs() { if (beginTimeMs.get() == 0) return -1; long beginTime = beginTimeMs.get(); long curOrCompletedTime; if (transactionState == STATE.SUCCEEDED || transactionState == STATE.FAILED) curOrCompletedTime = endTimeMs.get(); else curOrCompletedTime = System.currentTimeMillis(); long durationMs = curOrCompletedTime - beginTime; return (durationMs / 1000.0d); } /** * Gets the duration of the last execute call. * Only provides meaningful time after execute and before end or a retry of execute. * To be used by transaction controller only. * @return duration if execution completed or -1 if executing or never executed. */ public double getExecutionTimeSecs() { double durationSecs = (executeEndTimeMs.get() - executeStartTimeMs.get()) / 1000.0d; return durationSecs > 0? durationSecs : - 1; } /** * Get's the duration from the most recent execute call until now. * To determine stale transactions. * @return -1 if not currently executing */ public double getCurrentExecutionTimeSecs() { if (transactionState == STATE.SUCCEEDED || transactionState == STATE.FAILED || transactionState == STATE.NOTEXECUTED) return -1; long durationMs = System.currentTimeMillis() - executeStartTimeMs.get(); return (durationMs / 1000.0d); } /** * Adds a transaction event listener to the toplevel transaction. * Listeners can only be registered during begin and end. * @param l */ public void addTopLevelEventListener(CirmTransactionListener l) { CirmTransaction<?> toplevel = get(); if (toplevel == null) throw new IllegalStateException("A listener may only be registered with the toplevel transaction during begin and end of a transaction."); toplevel.transactionEventSupport.addListener(l); } /** * Removes a transaction event listener from the toplevel transaction. * Listeners can only be registered during begin and end. * @param l */ public void removeTopLevelEventListener(CirmTransactionListener l) { CirmTransaction<?> toplevel = get(); if (toplevel == null) throw new IllegalStateException("A listener may only be registered with the toplevel transaction during begin and end of a transaction."); toplevel.transactionEventSupport.addListener(l); } /** * Clears all listeners from the toplevel transaction. * This happens automatically for each retry. */ public void clearTopLevelEventListeners() { CirmTransaction<?> toplevel = get(); if (toplevel == null) throw new IllegalStateException("A listener may only be registered with the toplevel transaction during begin and end of a transaction."); toplevel.transactionEventSupport.clearListeners(); } public CirmTransactionEventSupport getTransactionEventSupport() { return this.transactionEventSupport; } }