Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added transaction propagation except nested, split Transaction.java a…

…nd TransactionScope.java

Signed-off-by: gburgett <gordon.burgett@gmail.com>
  • Loading branch information...
commit 20d2ce4834fedcff60b1f083e71d51d9568bc01a 1 parent f2b5817
@gburgett authored
Showing with 1,303 additions and 248 deletions.
  1. +58 −0 java/XFlat/src/org/xflatdb/xflat/DatabaseConfig.java
  2. +6 −0 java/XFlat/src/org/xflatdb/xflat/TableConfig.java
  3. +30 −11 java/XFlat/src/org/xflatdb/xflat/db/EngineBase.java
  4. +11 −0 java/XFlat/src/org/xflatdb/xflat/db/EngineTransactionManager.java
  5. +3 −2 java/XFlat/src/org/xflatdb/xflat/engine/CachedDocumentEngine.java
  6. +6 −1 java/XFlat/src/org/xflatdb/xflat/transaction/Isolation.java
  7. +38 −0 java/XFlat/src/org/xflatdb/xflat/transaction/Propagation.java
  8. +443 −94 java/XFlat/src/org/xflatdb/xflat/transaction/ThreadContextTransactionManager.java
  9. +22 −55 java/XFlat/src/org/xflatdb/xflat/transaction/Transaction.java
  10. +13 −11 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionManager.java
  11. +58 −7 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionOptions.java
  12. +31 −0 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionPropagationException.java
  13. +90 −0 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionScope.java
  14. +15 −14 java/XFlat/test/org/xflatdb/xflat/db/EngineTestsBase.java
  15. +477 −52 java/XFlat/test/org/xflatdb/xflat/db/EngineTransactionManagerTestBase.java
  16. +2 −1  java/XFlat/test/org/xflatdb/xflat/transaction/ThreadContextTransactionManagerTest.java
View
58 java/XFlat/src/org/xflatdb/xflat/DatabaseConfig.java
@@ -19,6 +19,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import org.xflatdb.xflat.convert.PojoConverter;
import org.xflatdb.xflat.db.IdGenerator;
import org.xflatdb.xflat.db.IntegerIdGenerator;
@@ -176,4 +177,61 @@ public DatabaseConfig setDefaultTableConfig(TableConfig tableConfig){
ret.defaultTableConfig = tableConfig;
return ret;
}
+
+ /**
+ * Gets the default database config. Equivalent to instantiating
+ * a new instance, but this is a singleton.
+ */
+ public static DatabaseConfig DEFAULT = new DatabaseConfig();
+
+ @Override
+ public int hashCode() {
+ int hash = 3;
+ hash = 43 * hash + this.threadCount;
+ hash = 43 * hash + Objects.hashCode(this.pojoConverterClass);
+ hash = 43 * hash + Objects.hashCode(this.defaultTableConfig);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DatabaseConfig other = (DatabaseConfig) obj;
+ if(this.idGeneratorStrategy == null){
+ if(other.idGeneratorStrategy != null)
+ return false;
+ }
+ else{
+ if(other.idGeneratorStrategy == null){
+ return false;
+ }
+
+ if(this.idGeneratorStrategy.size() != other.idGeneratorStrategy.size()){
+ return false;
+ }
+
+ for(int i = 0; i < this.idGeneratorStrategy.size(); i++){
+ if(!Objects.equals(this.idGeneratorStrategy.get(i), other.idGeneratorStrategy.get(i)))
+ return false;
+ }
+ }
+
+ if (this.threadCount != other.threadCount) {
+ return false;
+ }
+ if (!Objects.equals(this.pojoConverterClass, other.pojoConverterClass)) {
+ return false;
+ }
+ if (!Objects.equals(this.defaultTableConfig, other.defaultTableConfig)) {
+ return false;
+ }
+ return true;
+ }
+
+
}
View
6 java/XFlat/src/org/xflatdb/xflat/TableConfig.java
@@ -108,6 +108,12 @@ public TableConfig sharded(ShardsetConfig<?> config){
//TODO: future configuration options
+ /**
+ * Gets the default table config. Equivalent to instantiating
+ * a new instance, but this is a singleton.
+ */
+ public static TableConfig DEFAULT = new TableConfig();
+
@Override
public int hashCode() {
View
41 java/XFlat/src/org/xflatdb/xflat/db/EngineBase.java
@@ -34,6 +34,7 @@
import org.xflatdb.xflat.transaction.Transaction;
import org.xflatdb.xflat.transaction.TransactionException;
import org.xflatdb.xflat.transaction.TransactionManager;
+import org.xflatdb.xflat.transaction.TransactionOptions;
/**
* The base class for Engine objects. The Database uses the functionality
@@ -232,10 +233,19 @@ protected Transaction ensureWriteReady(){
}
Transaction tx = this.transactionManager.getTransaction();
- if(tx != null && tx.getOptions().getReadOnly()){
- throw new TransactionException("Cannot write in a read-only transaction");
+ if(tx != null){
+ if(tx.isReadOnly()){
+ throw new TransactionException("Cannot write in a read-only transaction");
+ }
+ if(tx.isCommitted()){
+ throw new TransactionException("Cannot write in an already committed transaction");
+ }
+ if(tx.isReverted()){
+ throw new TransactionException("Cannot write in an already reverted transaction");
+ }
}
+
//check the engine state
EngineState state = this.state.get();
if(state == EngineState.SpunDown ||
@@ -417,7 +427,7 @@ protected void setId(Element row, String id){
* After this method returns, the data should be stored in non-volatile storage.
* @param tx
*/
- public void commit(Transaction tx){
+ public void commit(Transaction tx, TransactionOptions options){
}
@@ -467,16 +477,16 @@ public Row(String id, RowData data){
/**
* Chooses the most recent committed RowData that was committed before the given transaction.
* If the transaction is null, this will choose the most recent committed
- * RowData globally.
+ * RowData globally. This is
* <p/>
* ALWAYS invoke this while synchronized on the Row.
* @param currentTransaction The current transaction, or null.
- * @param transactionId The transaction ID to use if the current transaction is null
+ * @param transactionId The transaction ID to use iff the current transaction is null.
+ * This is overwritten if the transaction is not null.
* @return The most recent committed RowData in this row, committed before the transaction.
*/
public RowData chooseMostRecentCommitted(Transaction currentTransaction, long transactionId){
if(currentTransaction != null){
- //override the given transaction ID just in case
transactionId = currentTransaction.getTransactionId();
}
@@ -509,11 +519,16 @@ public RowData chooseMostRecentCommitted(Transaction currentTransaction, long tr
if(transactionId > data.commitId){
//the current transaction is null or began after the transaction was committed
- if(retCommitId < data.commitId){
- //the last valid version we saw was before this version.
+ //check if the transaction is an in-progress commit
+ if(!transactionManager.isCommitInProgress(data.transactionId)){
+ //the transaction is wholly committed.
+
+ if(retCommitId < data.commitId){
+ //the last valid version we saw was before this version.
- ret = data;
- retCommitId = data.commitId;
+ ret = data;
+ retCommitId = data.commitId;
+ }
}
}
}
@@ -538,7 +553,7 @@ public RowData chooseMostRecentCommitted(Transaction currentTransaction, long tr
* @param snapshotId The Transaction ID representing the time at which a snapshot of the data should be obtained.
* @return The most recent committed RowData in this row, committed before the given snapshot.
*/
- public RowData chooseMostRecentCommitted(Long snapshotId){
+ public RowData chooseMostRecentCommitted(long snapshotId){
return chooseMostRecentCommitted(null, snapshotId);
}
@@ -574,6 +589,10 @@ public boolean cleanup(){
}
}
+ //the data might be committed
+ if(transactionManager.isCommitInProgress(data.transactionId))
+ continue;
+
//the data is committed
if(mostRecent == null){
View
11 java/XFlat/src/org/xflatdb/xflat/db/EngineTransactionManager.java
@@ -77,6 +77,17 @@
public abstract long isTransactionCommitted(long transactionId);
/**
+ * Checks to see if the given transaction ID is not yet finished committing.
+ * <p/>
+ * If true, then the transaction has a commit ID assigned and some rows may
+ * be marked with that commit ID, but {@link org.xflatdb.xflat.transaction.Transaction#isCommitted() }
+ * will be false.
+ * @param transactionId The ID of the transaction to check.
+ * @return true if the transaction has begun committing but is not yet finished.
+ */
+ public abstract boolean isCommitInProgress(long transactionId);
+
+ /**
* Checks to see if the given transaction ID has been reverted. If so,
* returns true, otherwise false.
* </p>
View
5 java/XFlat/src/org/xflatdb/xflat/engine/CachedDocumentEngine.java
@@ -51,6 +51,7 @@
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
+import org.xflatdb.xflat.transaction.TransactionOptions;
/**
* This is an engine that caches the entire table in memory as a JDOM {@link Document}.
@@ -528,7 +529,7 @@ private void updateTask(){
private AtomicLong currentlyCommitting = new AtomicLong(-1);
@Override
- public void commit(Transaction tx){
+ public void commit(Transaction tx, TransactionOptions options){
synchronized(syncRoot){
if(!currentlyCommitting.compareAndSet(-1, tx.getTransactionId())){
//see if this transaction is completely finished committing, or if it reverted
@@ -548,7 +549,7 @@ public void commit(Transaction tx){
if(log.isTraceEnabled())
this.log.trace("committing row " + row.rowId);
synchronized(row){
- if(tx.getOptions().getIsolationLevel() == Isolation.SNAPSHOT){
+ if(options.getIsolationLevel() == Isolation.SNAPSHOT){
//check for conflicts
for(RowData data : row.rowData.values()){
if(data.commitId > tx.getTransactionId()){
View
7 java/XFlat/src/org/xflatdb/xflat/transaction/Isolation.java
@@ -21,7 +21,12 @@
*/
public enum Isolation {
/**
- * Snapshot isolation level.
+ * Represents an isolation level of Snapshot. <br/>
+ * Snapshot isolation level means that operations within the transaction
+ * context operate on a "snapshot" of the data taken at the time the transaction
+ * was opened. <br/>
+ * When the transaction is committed, if concurrent operations have modified the
+ * same rows a {@link WriteConflictException} is thrown.
* <p/>
* see <a href="http://en.wikipedia.org/wiki/Snapshot_isolation">http://en.wikipedia.org/wiki/Snapshot_isolation</a>
*/
View
38 java/XFlat/src/org/xflatdb/xflat/transaction/Propagation.java
@@ -0,0 +1,38 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.xflatdb.xflat.transaction;
+
+/**
+ * Represents the different propagation behavior for a transaction. <br/>
+ * Propagation defines the behavior of {@link TransactionManager#openTransaction() }
+ * when a transaction already exists.
+ * <p/>
+ * see http://static.springsource.org/spring/docs/3.2.x/javadoc-api/
+ * @author Gordon
+ */
+public enum Propagation {
+ /** Support a current transaction, throw an exception if none exists. */
+ MANDATORY,
+
+ /** Execute within a nested transaction if a current transaction exists, behave like REQUIRED else. */
+ NESTED,
+
+ /** Execute non-transactionally, throw an exception if a transaction exists. */
+ NEVER,
+
+ /** Execute non-transactionally, suspend the current transaction if one exists. */
+ NOT_SUPPORTED,
+
+ /** Support a current transaction, create a new one if none exists.
+ * This is the default propagation behavior.
+ */
+ REQUIRED,
+
+ /** Create a new transaction, suspend the current transaction if one exists. Analogous to EJB transaction attribute of the same name. */
+ REQUIRES_NEW,
+
+ /** Support a current transaction, execute non-transactionally if none exists. */
+ SUPPORTS
+}
View
537 java/XFlat/src/org/xflatdb/xflat/transaction/ThreadContextTransactionManager.java
@@ -18,17 +18,21 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
import org.xflatdb.xflat.XFlatException;
import org.xflatdb.xflat.convert.ConversionException;
import org.xflatdb.xflat.convert.Converter;
@@ -36,9 +40,6 @@
import org.xflatdb.xflat.db.EngineTransactionManager;
import org.xflatdb.xflat.db.XFlatDatabase;
import org.xflatdb.xflat.util.DocumentFileWrapper;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
/**
* A {@link TransactionManager} that uses the current thread as the context for transactions.
@@ -51,9 +52,9 @@
*/
public class ThreadContextTransactionManager extends EngineTransactionManager {
- private Map<Long, ThreadedTransaction> currentTransactions = new ConcurrentHashMap<>();
+ private Map<Long, AmbientThreadedTransactionScope> currentTransactions = new ConcurrentHashMap<>();
- private Map<Long, ThreadedTransaction> committedTransactions = new ConcurrentHashMap<>();
+ private Map<Long, AmbientThreadedTransactionScope> committedTransactions = new ConcurrentHashMap<>();
private DocumentFileWrapper journalWrapper;
private Document transactionJournal = null;
@@ -71,7 +72,7 @@ public ThreadContextTransactionManager(DocumentFileWrapper wrapper){
}
/**
- * Gets the Id of the current context, which is the current thread's ID.
+ * Gets the Id of the current context, which is the current thread's ID.
* @return The current thread's ID.
*/
protected Long getContextId(){
@@ -80,40 +81,108 @@ protected Long getContextId(){
@Override
public Transaction getTransaction() {
- return currentTransactions.get(getContextId());
+ TransactionBase tx = currentTransactions.get(getContextId());
+ if(tx == null || tx.getOptions().getPropagation() == Propagation.NOT_SUPPORTED)
+ return null;
+
+ return tx.getTransaction();
}
@Override
- public Transaction openTransaction() {
- return openTransaction(TransactionOptions.Default);
+ public TransactionScope openTransaction() {
+ return openTransaction(TransactionOptions.DEFAULT);
}
@Override
- public Transaction openTransaction(TransactionOptions options) {
- if(currentTransactions.get(getContextId()) != null){
- throw new IllegalStateException("Transaction already open on current thread.");
- }
+ public TransactionScope openTransaction(TransactionOptions options) {
- ThreadedTransaction ret = new ThreadedTransaction(generateNewId(), options);
- if(currentTransactions.put(getContextId(), ret) != null){
- //how could this happen? I dunno, programs surprise me all the time.
- throw new IllegalStateException("Transaction already open on current thread");
- }
+ AmbientThreadedTransactionScope ret;
+ long contextId = getContextId();
- return ret;
+ switch(options.getPropagation()){
+ case MANDATORY:
+ ret = currentTransactions.get(contextId);
+ if(ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED){
+ throw new TransactionPropagationException("propagation MANDATORY, but no current transaction.");
+ }
+ //use the current transaction, with a wrapper to prevent
+ //this instance from closing it prematurely.
+ return new WrappingTransactionScope(ret, options.getReadOnly());
+
+ case NESTED:
+ throw new UnsupportedOperationException("Nested transactions not yet supported");
+
+ case NEVER:
+ ret = currentTransactions.get(contextId);
+ if(ret != null && ret.getOptions().getPropagation() != Propagation.NOT_SUPPORTED){
+ throw new TransactionPropagationException("propagation NEVER, but current transaction exists");
+ }
+ //return a shell object representing the non-transactional operation.
+ return new EmptyTransactionScope(options);
+
+ case NOT_SUPPORTED:
+ ret = currentTransactions.remove(contextId);
+ if(ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED){
+ //we are already operating non-transactionally, just need
+ //to return a shell object.
+ return new EmptyTransactionScope(options);
+ }
+ //need to return a shell that will also replace the suspended
+ //transaction when it is closed.
+ return new NotSupportedTransaction(ret, options);
+
+ case REQUIRED:
+ ret = currentTransactions.get(contextId);
+ if(ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED){
+ //no current transaction, create a new one
+ //suspending the NOT_SUPPORTED transaction if it exists.
+ ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options);
+ currentTransactions.put(contextId, ret);
+ }
+
+ //use the current transaction, with a wrapper to prevent
+ //this instance from closing it prematurely.
+ return new WrappingTransactionScope(ret, options.getReadOnly());
+
+
+ case REQUIRES_NEW:
+ //create a new transaction, suspending the current one if it exists.
+ ret = currentTransactions.get(contextId);
+ ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options);
+ currentTransactions.put(contextId, ret);
+
+ //use the current transaction, with a wrapper to prevent
+ //this instance from closing it prematurely.
+ return new WrappingTransactionScope(ret, options.getReadOnly());
+
+ case SUPPORTS:
+ ret = currentTransactions.get(contextId);
+ if(ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED){
+ //we are already operating non-transactionally, just need
+ //to return a shell object.
+ return new EmptyTransactionScope(options);
+ }
+
+ //use the current transaction, with a wrapper to prevent
+ //this instance from closing it prematurely.
+ return new WrappingTransactionScope(ret, options.getReadOnly());
+
+ default:
+ throw new UnsupportedOperationException("Propagation behavior not supported: " + options.getPropagation().toString());
+ }
}
@Override
public long isTransactionCommitted(long transactionId) {
- ThreadedTransaction tx = committedTransactions.get(transactionId);
- return tx == null ? -1 : tx.getCommitId();
+ TransactionBase tx = committedTransactions.get(transactionId);
+ return tx == null ? -1 : tx.commitId;
}
@Override
public boolean isTransactionReverted(long transactionId) {
//if we find it in the current transactions, check the transaction
- for(Transaction tx : currentTransactions.values()){
- if(tx.getTransactionId() == transactionId){
+ for(TransactionBase tx : currentTransactions.values()){
+ if(tx.id == transactionId){
return tx.isReverted();
}
}
@@ -136,9 +205,9 @@ public long transactionlessCommitId() {
@Override
public long getLowestOpenTransaction() {
long lowest = Long.MAX_VALUE;
- for(Transaction tx : currentTransactions.values()){
- if(tx.getTransactionId() < lowest){
- lowest = tx.getTransactionId();
+ for(TransactionBase tx : currentTransactions.values()){
+ if(tx.id < lowest){
+ lowest = tx.id;
}
}
@@ -148,7 +217,7 @@ public long getLowestOpenTransaction() {
@Override
public void bindEngineToCurrentTransaction(EngineBase engine) {
- ThreadedTransaction tx = currentTransactions.get(getContextId());
+ TransactionBase tx = currentTransactions.get(getContextId());
if(tx == null){
return;
}
@@ -163,10 +232,10 @@ public void bindEngineToCurrentTransaction(EngineBase engine) {
@Override
public synchronized void unbindEngineExceptFrom(EngineBase engine, Collection<Long> transactionIds) {
- Iterator<ThreadedTransaction> it = this.committedTransactions.values().iterator();
+ Iterator<AmbientThreadedTransactionScope> it = this.committedTransactions.values().iterator();
while(it.hasNext()){
- ThreadedTransaction tx = it.next();
- if(transactionIds.contains(tx.getTransactionId())){
+ TransactionBase tx = it.next();
+ if(transactionIds.contains(tx.id)){
continue;
}
@@ -193,7 +262,7 @@ private void loadJournal() throws IOException, JDOMException{
}
}
- private synchronized void commit(ThreadedTransaction tx){
+ private synchronized void commit(AmbientThreadedTransactionScope tx){
//journal the entry so we can recover if catastrophic failure occurs
TransactionJournalEntry entry = new TransactionJournalEntry();
entry.txId = tx.id;
@@ -219,7 +288,7 @@ private synchronized void commit(ThreadedTransaction tx){
for(EngineBase e : tx.boundEngines){
if(log.isTraceEnabled())
log.trace(String.format("committing transaction %d to table %s", tx.id, e.getTableName()));
- e.commit(tx);
+ e.commit(tx.getTransaction(), tx.getOptions());
}
}catch(Exception ex){
try{
@@ -315,44 +384,178 @@ public void recover(XFlatDatabase db) {
throw new XFlatException("Unable to recover", ex);
}
}
+
+ @Override
+ public boolean isCommitInProgress(long transactionId) {
+ TransactionBase tx = this.currentTransactions.get(transactionId);
+ if(tx == null)
+ return false;
+ //in-progress if the commit ID was assigned and the tx was not yet committed.
+ return tx.commitId != -1 && !tx.isCommitted();
+ }
+
/**
- * A Transaction that is meant to exist within the context of one thread.
- * There should be no cross-thread transactional data access, only cross-thread
- * state querying.
+ * The base class for the different types of transactions handled by this
+ * transaction manager. The different implementations are dependent on
+ * the propagation level used when the transaction was opened.
*/
- protected class ThreadedTransaction implements Transaction{
-
- private TransactionOptions options;
+ protected abstract class TransactionBase implements TransactionScope {
+ protected TransactionOptions options;
- private AtomicBoolean isCompleted = new AtomicBoolean(false);
- private AtomicBoolean isRollbackOnly = new AtomicBoolean(false);
+ protected AtomicBoolean isCompleted = new AtomicBoolean(false);
+ protected AtomicBoolean isRollbackOnly = new AtomicBoolean(false);
- private final long id;
+ protected volatile boolean isClosed = false;
- private AtomicReference<Set<TransactionListener>> listeners = new AtomicReference<>(null);
+ protected final long id;
+
+ protected AmbientThreadedTransactionScope suspended;
//we can get away with this being an unsynchronized HashSet because it will only ever be added to by one
//thread, and then only so long as the transaction is open, and then will be removed from
//by a different thread, but only one at a time, synchronized elsewhere, and after all adds are finished.
final Set<EngineBase> boundEngines = new HashSet<>();
- private long commitId = -1;
- @Override
- public long getCommitId(){
- return commitId;
+ protected AtomicReference<Set<TransactionListener>> listeners = new AtomicReference<>(null);
+
+ protected long commitId = -1;
+
+ //The transaction representing this transaction scope.
+ private final Transaction transaction = new Transaction(){
+ @Override
+ public long getTransactionId() {
+ return id;
+ }
+
+ @Override
+ public long getCommitId() {
+ return commitId;
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return TransactionBase.this.isCommitted();
+ }
+
+ @Override
+ public boolean isReverted() {
+ return TransactionBase.this.isReverted();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return options.getReadOnly();
+ }
+
+ };
+
+ public Transaction getTransaction(){
+ return transaction;
}
- protected ThreadedTransaction(long id, TransactionOptions options){
+
+ protected TransactionBase(long id, AmbientThreadedTransactionScope suspended, TransactionOptions options){
+ this.id = id;
+ this.suspended = suspended;
+
this.options = options;
if(this.options.getReadOnly()){
this.isRollbackOnly.set(true);
}
- this.id = id;
+ }
+
+ protected void fireEvent(int event){
+ Set<TransactionListener> listeners = this.listeners.get();
+ if(listeners == null)
+ return;
+
+ TransactionEventObject evtObj = new TransactionEventObject(ThreadContextTransactionManager.this, this.transaction, event);
+ synchronized(listeners){
+ for(Object l : listeners.toArray()){
+ ((TransactionListener)l).TransactionEvent(evtObj);
+ }
+ }
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return this.isCompleted.get() && commitId > -1;
+ }
+
+ @Override
+ public boolean isReverted() {
+ return isCompleted.get() && commitId == -1;
+ }
+
+ @Override
+ public void putTransactionListener(TransactionListener listener) {
+ Set<TransactionListener> l = this.listeners.get();
+ if(l == null){
+ l = new HashSet<>();
+ if(!this.listeners.compareAndSet(null, l)){
+ l = this.listeners.get();
+ }
+ }
+ synchronized(l){
+ l.add(listener);
+ }
+ }
+
+ @Override
+ public void removeTransactionListener(TransactionListener listener) {
+ Set<TransactionListener> l = this.listeners.get();
+ if(l == null){
+ return;
+ }
+ synchronized(l){
+ l.remove(listener);
+ }
+ }
+
+ @Override
+ public void setRevertOnly() {
+ this.isRollbackOnly.set(true);
+ }
+
+ @Override
+ public TransactionOptions getOptions() {
+ return this.options;
+ }
+
+
+ @Override
+ public void close() {
+ if(suspended != null && !suspended.isClosed){
+ //need to put back the suspended transaction
+ ThreadContextTransactionManager.this.currentTransactions.put(getContextId(), suspended);
+ suspended = null;
+ }
+
+ this.isClosed = true;
+ }
+ }
+
+ /**
+ * A Transaction that is meant to exist within the context of one thread.
+ * There should be no cross-thread transactional data access, only cross-thread
+ * state querying.
+ */
+ protected class AmbientThreadedTransactionScope extends TransactionBase {
+
+ private List<WrappingTransactionScope> uncommittedScopes = new LinkedList<>();
+ private List<WrappingTransactionScope> wrappingScopes = new LinkedList<>();
+
+ protected AmbientThreadedTransactionScope(long id, AmbientThreadedTransactionScope suspended, TransactionOptions options){
+ super(id, suspended, options);
}
@Override
public void commit() throws TransactionException {
+ throw new UnsupportedOperationException("should not be called directly");
+ }
+
+ private void doCommit() throws TransactionException {
if(this.isRollbackOnly.get()){
throw new IllegalTransactionStateException("Cannot commit a rollback-only transaction");
}
@@ -362,104 +565,248 @@ public void commit() throws TransactionException {
commitId = generateNewId();
ThreadContextTransactionManager.this.commit(this);
+
//soon as commit returns, we are committed.
this.isCompleted.set(true);
fireEvent(TransactionEventObject.COMMITTED);
}
-
+
+ void completeWrappingScope(WrappingTransactionScope scope){
+ if(uncommittedScopes.remove(scope) && uncommittedScopes.isEmpty()){
+ //all wrapping transaction scopes have completed, we can commit
+ doCommit();
+ }
+ //otherwise we do nothing, simply mark the wrapping scope as completed by removing it from the list.
+ }
+
+ void addWrappingScope(WrappingTransactionScope scope){
+ synchronized(this){
+ //add them at the beginning because the most recent ones
+ //are most likely to close first.
+ if(!scope.getOptions().getReadOnly()){
+ //only add it to uncommitted scopes if it can actually write.
+ //ReadOnly scopes can close without commit, but an explicit revert
+ //will still revert the entire ambient scope.
+ uncommittedScopes.add(0, scope);
+ }
+ wrappingScopes.add(0, scope);
+ }
+ }
+
+
@Override
public void revert() {
if(!isCompleted.compareAndSet(false, true)){
throw new IllegalTransactionStateException("Cannot rollback a completed transaction");
}
- ThreadContextTransactionManager.this.revert(this.boundEngines, this.id, false);
+ doRevert();
fireEvent(TransactionEventObject.REVERTED);
}
- @Override
- public void setRevertOnly() {
- this.isRollbackOnly.set(true);
+ public void doRevert(){
+ ThreadContextTransactionManager.this.revert(this.boundEngines, this.id, false);
}
+
+ void closeWrappingScope(WrappingTransactionScope scope){
+ synchronized(this){
+ if(uncommittedScopes.remove(scope) && !this.isCompleted.get()){
+ //the scope was uncommitted, need to revert the transaction
+ revert();
+ }
- @Override
- public long getTransactionId() {
- return this.id;
+ if(wrappingScopes.remove(scope) && wrappingScopes.isEmpty()){
+ //all wrapping transaction scopes have closed, we can close.
+ close();
+ }
+ //otherwise, can't close yet, still have some open scopes.
+ }
}
-
+
@Override
- public void close() {
+ public void close(){
if(isCompleted.compareAndSet(false, true)){
//we completed in the close, need to revert.
- ThreadContextTransactionManager.this.revert(this.boundEngines, this.id, false);
+ doRevert();
}
- //remove the transaction from the current transactions map
- Iterator<ThreadedTransaction> it = currentTransactions.values().iterator();
+ //remove the transaction scope from the current transactions map
+ Iterator<AmbientThreadedTransactionScope> it = currentTransactions.values().iterator();
while(it.hasNext()){
+ //Object equality because we don't know which
if(it.next() == this){
it.remove();
+ break;
}
}
+
+ super.close();
+ }
+ }
+
+ /**
+ * A transaction that implements the {@link Propagation#NOT_SUPPORTED} behavior,
+ * maintaining a reference to the suspended transaction so that it can be
+ * replaced when this is closed.
+ */
+ protected class NotSupportedTransaction extends TransactionBase {
+ public NotSupportedTransaction(AmbientThreadedTransactionScope suspended, TransactionOptions options){
+ super(-1, suspended, options);
+ }
+
+ @Override
+ public void commit() throws TransactionException {
+ throw new TransactionException("Cannot commit a transaction opened with propagation " +
+ "NEVER or NOT_SUPPORTED");
}
@Override
+ public void revert() {
+ throw new TransactionException("Cannot revert a transaction opened with propagation " +
+ "NEVER or NOT_SUPPORTED");
+ }
+
+ }
+
+ /**
+ * A transaction object that represents no open transaction. This is
+ * created by opening a transaction with the {@link Propagation#NEVER} or
+ * with {@link Propagation#NOT_SUPPORTED} when the
+ */
+ protected class EmptyTransactionScope implements TransactionScope{
+
+ private TransactionOptions options;
+
+ private volatile boolean isCommitted = false;
+ private volatile boolean isReverted = false;
+ private volatile boolean isClosed = false;
+
+
+ public EmptyTransactionScope(TransactionOptions options){
+ this.options = options;
+ }
+
+
+ @Override
+ public void commit() throws TransactionException {
+ throw new TransactionException("Cannot commit a transaction opened with propagation " +
+ "NEVER or NOT_SUPPORTED");
+ }
+
+ @Override
+ public void revert() {
+ throw new TransactionException("Cannot revert a transaction opened with propagation " +
+ "NEVER or NOT_SUPPORTED");
+ }
+
+ @Override
+ public void setRevertOnly() {
+
+ }
+
+ @Override
public boolean isCommitted() {
- return this.isCompleted.get() && commitId > -1;
+ return isCommitted;
}
@Override
public boolean isReverted() {
- return isCompleted.get() && commitId == -1;
+ return isReverted;
+ }
+
+ @Override
+ public TransactionOptions getOptions() {
+ return options;
+ }
+
+ @Override
+ public void close() {
+ //nothing to do
+ isClosed = true;
}
@Override
public void putTransactionListener(TransactionListener listener) {
- Set<TransactionListener> l = this.listeners.get();
- if(l == null){
- l = new HashSet<>();
- if(!this.listeners.compareAndSet(null, l)){
- l = this.listeners.get();
- }
- }
- synchronized(l){
- l.add(listener);
- }
+
}
@Override
public void removeTransactionListener(TransactionListener listener) {
- Set<TransactionListener> l = this.listeners.get();
- if(l == null){
- return;
- }
- synchronized(l){
- l.remove(listener);
- }
+
+ }
+
+
+ }
+
+ /**
+ * A TransactionScope object that provides a view onto the ambient scope.
+ * There may be multiple wrapping transaction scopes all pointing to the
+ * same ambient transaction scope. When ALL of these is committed, the
+ * underlying ambient transaction is committed.
+ */
+ protected class WrappingTransactionScope implements TransactionScope {
+
+ private AmbientThreadedTransactionScope wrapped;
+
+ private TransactionOptions options;
+
+ protected WrappingTransactionScope(AmbientThreadedTransactionScope wrapped, boolean isReadOnly){
+ this.wrapped = wrapped;
+ this.options = wrapped.getOptions().withReadOnly(isReadOnly);
+
+ wrapped.addWrappingScope(this);
+ }
+
+ @Override
+ public void commit() throws TransactionException {
+ wrapped.completeWrappingScope(this);
+ }
+
+ @Override
+ public void revert() {
+ wrapped.revert();
+ }
+
+ @Override
+ public void setRevertOnly() {
+ wrapped.setRevertOnly();
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return wrapped.isCommitted();
+ }
+
+ @Override
+ public boolean isReverted() {
+ return wrapped.isReverted();
}
@Override
public TransactionOptions getOptions() {
return this.options;
}
-
- private void fireEvent(int event){
- Set<TransactionListener> listeners = this.listeners.get();
- if(listeners == null)
- return;
-
- TransactionEventObject evtObj = new TransactionEventObject(ThreadContextTransactionManager.this, this, event);
- synchronized(listeners){
- for(Object l : listeners.toArray()){
- ((TransactionListener)l).TransactionEvent(evtObj);
- }
- }
+
+ @Override
+ public void close() {
+ wrapped.closeWrappingScope(this);
}
+
+ @Override
+ public void putTransactionListener(TransactionListener listener) {
+ wrapped.putTransactionListener(listener);
+ }
+
+ @Override
+ public void removeTransactionListener(TransactionListener listener) {
+ wrapped.removeTransactionListener(listener);
+ }
+
}
-
+ //<editor-fold desc="transaction journal">
private class TransactionJournalEntry{
public long txId;
public long commitId;
@@ -510,4 +857,6 @@ public TransactionJournalEntry convert(Element source) throws ConversionExceptio
return ret;
}
};
+
+ //</editor-fold>
}
View
77 java/XFlat/src/org/xflatdb/xflat/transaction/Transaction.java
@@ -28,35 +28,7 @@
* reverted.
* @author Gordon
*/
-public interface Transaction extends AutoCloseable {
-
- /**
- * Commits the transaction immediately. Transactions are committed atomically
- * and durably, so that the instant this method returns successfully, the caller can be
- * assured that the modifications performed by this transaction have been
- * saved to disk.
- *
- * @throws TransactionException if an error occurred during the commit. The
- * transaction manager will automatically revert the transaction upon a commit
- * error.
- * @throws IllegalTransactionStateException if the transaction has already been
- * committed, reverted, or is revert only.
- */
- void commit() throws TransactionException;
-
- /**
- * Reverts the transaction immediately. When a transaction is reverted,
- * the database acts as though all the modifications performed inside the
- * transaction scope never happened.
- */
- void revert();
-
- /**
- * Sets the transaction to be "Revert Only". The transaction will continue
- * as normal, but will throw an {@link IllegalStateException} if {@link #commit() }
- * is called.
- */
- void setRevertOnly();
+public interface Transaction {
/**
* Gets the ID of this transaction. A Transaction's ID is linked to the time
@@ -76,40 +48,35 @@
long getCommitId();
/**
- * Returns true if the transaction has been committed.
- * @return true iff the transaction successfully committed.
+ * Returns true if the transaction scope to which this transaction belongs has been committed.
+ * <p/>
+ * This refers to the
+ * entire transaction scope; if this transaction's scope was opened within
+ * a larger transaction scope (except
+ * by {@link Propagation#NESTED}), then calling {@link #commit() } on this object
+ * will not result in a commit and this will return false until the larger
+ * scope is committed.
+ * @return true iff the entire ambient transaction successfully committed.
*/
boolean isCommitted();
/**
- * Returns true if the transaction has been reverted.
- * @return true iff the transaction was reverted.
+ * Returns true if the transaction scope to which this transaction belongs has been reverted.
+ * <p/>
+ * If this transaction's scope was propagated from a larger transaction scope (except
+ * by {@link Propagation#NESTED}), then this will return true if {@link #revert() }
+ * has been called on ANY transaction scope participating in the transaction.
+ * @return true iff the entire ambient transaction was reverted.
*/
boolean isReverted();
/**
- * Gets the options with which this transaction was opened.
- * @Return the TransactionOptions object provided when the transaction
- * was opened.
+ * Returns true if the current transaction scope was opened with the ReadOnly
+ * option set.
+ * @see TransactionOptions#getReadOnly()
+ * @return true if the current transaction is read only.
*/
- TransactionOptions getOptions();
+ boolean isReadOnly();
- /**
- * Closes the current transaction. If the transaction has yet to be committed
- * or reverted, the transaction is reverted immediately.
- */
- @Override
- void close();
-
- /**
- * Adds a transaction listener for this transaction, if it does not already exist.
- * @param listener The listener to add to this transaction.
- */
- void putTransactionListener(TransactionListener listener);
-
- /**
- * Removes a transaction listener for this transaction.
- * @param listener The listener to remove from this transaction.
- */
- void removeTransactionListener(TransactionListener listener);
+
}
View
24 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionManager.java
@@ -23,32 +23,34 @@
public interface TransactionManager {
/**
- * Gets the current transaction, or null if none exists. The current
- * transaction is defined as the transaction retrieved by the last call to
- * {@link #openTransaction(org.xflatdb.xflat.transaction.TransactionOptions) }
- * in this context (usually a thread context).
- * @return The current transaction, or null.
+ * Gets the current transaction representing the ambient transaction scope,
+ * or null if the current scope is transactionless.
*/
public Transaction getTransaction();
/**
- * Opens a new transaction, using the {@link TransactionOptions#Default} options.
+ * Opens a new transaction scope, using the {@link TransactionOptions#Default} options.
* If a transaction is already open in this context, an IllegalStateException
* is thrown.
* @return A new Transaction object representing the transaction open in this context.
* @throws IllegalStateException if a transaction is already open in this context.
*/
- public Transaction openTransaction();
+ public TransactionScope openTransaction();
/**
- * Opens a new transaction, using the given TransactionOptions. If a
- * transaction is already open in this context, an IllegalStateException
- * is thrown.
+ * Opens a new transaction scope, using the given TransactionOptions. A
+ * TransactionScope will create or participate in an ambient transaction,
+ * depending on the Propagation options.
+ * <p/>
+ * The ambient transaction is committed
+ * only when all its associated TransactionScopes are committed.<br/>
+ * If any TransactionScope is reverted, the entire ambient transaction is reverted.<br/>
+ * The ambient transaction is destroyed when the last TransactionScope is closed.
* @param options The TransactionOptions to apply to this transaction.
* @return A new Transaction object representing the transaction open in this context.
* @throws IllegalStateException if a transaction is already open in this context.
*/
- public Transaction openTransaction(TransactionOptions options);
+ public TransactionScope openTransaction(TransactionOptions options);
}
View
65 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionOptions.java
@@ -16,7 +16,9 @@
package org.xflatdb.xflat.transaction;
/**
- * An object representing the options that can be applied to a transaction.
+ * An object representing the options that can be applied to a transaction. <br/>
+ * Note that these options may be ignored if a currently open transaction is
+ * propagated. Control this with the {@link #getPropagation() Propagation} option.
* <p/>
* The TransactionOptions object is immutable; all set methods return a new
* instance that has the given option set.
@@ -42,6 +44,12 @@ public Isolation getIsolationLevel(){
return this.isolation;
}
+ private Propagation propagation;
+
+
+ public Propagation getPropagation(){
+ return propagation;
+ }
/**
* Sets whether this transaction is Read Only. A ReadOnly transaction
@@ -77,24 +85,67 @@ public TransactionOptions withIsolationLevel(Isolation level){
}
/**
+ * Sets the propagation behavior to apply when opening this transaction.
+ * The propagation behavior determines how the transaction manager should
+ * react to the current transaction scope when creating this transaction.
+ *
+ * @param propagation The propagation to apply when this transaction is opened.
+ * @return a new TransactionOptions object with Propagation == the given value.
+ */
+ public TransactionOptions withPropagation(Propagation propagation) {
+ TransactionOptions ret = new TransactionOptions(this);
+ ret.propagation = propagation;
+ return ret;
+ }
+
+ /**
* Creates a new TransactionOptions object with the default options.
*/
public TransactionOptions(){
this.readOnly = false;
this.isolation = Isolation.SNAPSHOT;
+ this.propagation = Propagation.REQUIRED;
}
private TransactionOptions(TransactionOptions other){
this.readOnly = other.readOnly;
this.isolation = other.isolation;
+ this.propagation = other.propagation;
}
/**
- * The default transaction options.
- * <p/>
- * ReadOnly: false, <br/>
- * IsolationLevel: SNAPSHOT <br/>
+ * Gets the default transaction options. Equivalent to instantiating
+ * a new instance.
*/
- public static final TransactionOptions Default = new TransactionOptions();
-
+ public static TransactionOptions DEFAULT = new TransactionOptions();
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 97 * hash + (this.readOnly ? 1 : 0);
+ hash = 97 * hash + (this.isolation != null ? this.isolation.hashCode() : 0);
+ hash = 97 * hash + (this.propagation != null ? this.propagation.hashCode() : 0);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final TransactionOptions other = (TransactionOptions) obj;
+ if (this.readOnly != other.readOnly) {
+ return false;
+ }
+ if (this.isolation != other.isolation) {
+ return false;
+ }
+ if (this.propagation != other.propagation) {
+ return false;
+ }
+ return true;
+ }
}
View
31 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionPropagationException.java
@@ -0,0 +1,31 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.xflatdb.xflat.transaction;
+
+/**
+ *
+ * @author Gordon
+ */
+public class TransactionPropagationException extends TransactionException {
+
+ /**
+ * Creates a new instance of
+ * <code>TransactionPropagationException</code> without detail message.
+ */
+ public TransactionPropagationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Constructs an instance of
+ * <code>TransactionPropagationException</code> with the specified detail
+ * message.
+ *
+ * @param msg the detail message.
+ */
+ public TransactionPropagationException(String msg) {
+ super(msg);
+ }
+}
View
90 java/XFlat/src/org/xflatdb/xflat/transaction/TransactionScope.java
@@ -0,0 +1,90 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.xflatdb.xflat.transaction;
+
+/**
+ * Represents a TransactionScope.
+ * @author Gordon
+ */
+public interface TransactionScope extends AutoCloseable {
+
+ /**
+ * Commits the transaction immediately. Transactions are committed atomically
+ * and durably, so that the instant this method returns successfully, the caller can be
+ * assured that the modifications performed by this transaction have been
+ * saved to disk.
+ *
+ * @throws TransactionException if an error occurred during the commit. The
+ * transaction manager will automatically revert the transaction upon a commit
+ * error.
+ * @throws IllegalTransactionStateException if the transaction has already been
+ * committed, reverted, or is revert only.
+ */
+ void commit() throws TransactionException;
+
+ /**
+ * Reverts the transaction immediately. When a transaction is reverted,
+ * the database acts as though all the modifications performed inside the
+ * transaction scope never happened.
+ */
+ void revert();
+
+ /**
+ * Sets the transaction to be "Revert Only". The transaction will continue
+ * as normal, but will throw an {@link IllegalStateException} if {@link #commit() }
+ * is called.
+ */
+ void setRevertOnly();
+
+ /**
+ * Returns true if the transaction scope has been committed.
+ * <p/>
+ * This refers to the
+ * entire transaction scope; if this transaction object was opened within
+ * a larger transaction scope, then calling {@link #commit() } on this object
+ * will not result in a commit and this will return false until the larger
+ * scope is committed.
+ * @return true iff the entire ambient transaction successfully committed.
+ */
+ boolean isCommitted();
+
+ /**
+ * Returns true if the transaction scope has been reverted.
+ * <p/>
+ * If this transaction was propagated from a larger transaction scope (except
+ * by {@link Propagation#NESTED}), then this will return true if {@link #revert() }
+ * has been called on ANY transaction object participating in the transaction.
+ * @return true iff the entire ambient transaction was reverted.
+ */
+ boolean isReverted();
+
+ /**
+ * Gets the options with which this transaction was opened.
+ * @Return the TransactionOptions object provided when the transaction
+ * was opened.
+ */
+ TransactionOptions getOptions();
+
+
+ /**
+ * Adds a transaction listener for the ambient transaction, if it does not already exist.
+ * @param listener The listener to add to the entire ambient transaction.
+ */
+ void putTransactionListener(TransactionListener listener);
+
+ /**
+ * Removes a transaction listener for the ambient transaction.
+ * @param listener The listener to remove from the entire ambient transaction.
+ */
+ void removeTransactionListener(TransactionListener listener);
+
+ /**
+ * Closes the current transaction scope. If the transaction has yet to be committed
+ * or reverted, and the transaction is the last participant in the ambient transaction scope
+ * (i.e. the root scope), the entire transaction is reverted immediately.
+ */
+ @Override
+ void close();
+}
View
29 java/XFlat/test/org/xflatdb/xflat/db/EngineTestsBase.java
@@ -15,9 +15,6 @@
*/
package org.xflatdb.xflat.db;
-import org.xflatdb.xflat.db.XFlatDatabase;
-import org.xflatdb.xflat.db.EngineBase;
-import org.xflatdb.xflat.db.EngineState;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -63,6 +60,7 @@
import org.junit.Test;
import test.Utils;
import static org.mockito.Mockito.*;
+import org.xflatdb.xflat.transaction.TransactionScope;
/**
*
@@ -1259,7 +1257,7 @@ public void testUpdate_InTransaction_RevertRemovesData() throws Exception {
XPathUpdate update = XPathUpdate.set(xpath.compile("third"), "updated text");
//ACT
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
int result = ctx.instance.update(query, update);
@@ -1303,7 +1301,7 @@ public void testReplaceRow_InTransaction_CommitModifiesData() throws Exception {
.setText("fourth text data");
//ACT
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
ctx.instance.replaceRow("0", fourth);
@@ -1340,7 +1338,7 @@ public void testDeleteRow_InTransaction_RevertReturnsRow() throws Exception {
spinUp(ctx);
//ACT
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
ctx.instance.deleteRow("0");
@@ -1373,7 +1371,7 @@ public void testInsert_InTransaction_HasReadIsolation() throws Exception {
Element outsideTransaction;
Element insideTransaction;
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
Element rowData = new Element("data").setText("some text data");
@@ -1429,7 +1427,7 @@ public void testQuery_InTransaction_HasReadIsolation() throws Exception {
List<Element> fromCursor = new ArrayList<>();
- try(Transaction tx = ctx.transactionManager.openTransaction(TransactionOptions.Default.withReadOnly(true))){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction(TransactionOptions.DEFAULT.withReadOnly(true))){
Element rowData = new Element("data")
.setAttribute("fooInt", "17")
@@ -1484,7 +1482,7 @@ public void testConflictingWrite_SnapshotIsolation_ThrowsWriteConflictException(
prepFileContents(ctx, null);
spinUp(ctx);
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
Element rowData = new Element("data").setText("some text data");
@@ -1516,7 +1514,7 @@ public void testConflictingWrite_SnapshotIsolation_TwoTransactions_ThrowsWriteCo
prepFileContents(ctx, null);
spinUp(ctx);
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
Element rowData = new Element("data").setText("some text data");
@@ -1524,7 +1522,7 @@ public void testConflictingWrite_SnapshotIsolation_TwoTransactions_ThrowsWriteCo
//swap out the transaction
ctx.transactionManager.setContextId(1L);
- try(Transaction tx2 = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx2 = ctx.transactionManager.openTransaction()){
//insert conflicting data
ctx.instance.insertRow("1", new Element("other").setText("other text data"));
@@ -1544,6 +1542,9 @@ public void testConflictingWrite_SnapshotIsolation_TwoTransactions_ThrowsWriteCo
}
}
}
+
+
+
//</editor-fold>
@@ -1564,7 +1565,7 @@ public void testCommit_NoSimultaneousTasks_DataIsCommittedImmediately() throws E
ctx.executorService = mock(ScheduledExecutorService.class);
ctx.instance.setExecutorService(ctx.executorService);
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
ctx.instance.insertRow("0", new Element("data").setText("some text data"));
ctx.instance.insertRow("1", new Element("second").setText("second text data"));
@@ -1614,7 +1615,7 @@ public int hashCode(){
}
@Override
- public void commit(Transaction tx){
+ public void commit(Transaction tx, TransactionOptions options){
//set up the second engine to throw an exception on commit
throw new RuntimeException("Test");
@@ -1700,7 +1701,7 @@ public int deleteAll(XPathQuery query) {
assertEquals("HashSet doesn't work as i thought", ctx.instance, it.next());
assertEquals("HashSet doesn't work as i thought", eng2, it.next());
- try(Transaction tx = ctx.transactionManager.openTransaction()){
+ try(TransactionScope tx = ctx.transactionManager.openTransaction()){
ctx.transactionManager.bindEngineToCurrentTransaction(eng2);
View
529 java/XFlat/test/org/xflatdb/xflat/db/EngineTransactionManagerTestBase.java
@@ -15,18 +15,18 @@
*/
package org.xflatdb.xflat.db;
-import org.xflatdb.xflat.db.XFlatDatabase;
-import org.xflatdb.xflat.db.EngineTransactionManager;
-import org.xflatdb.xflat.db.EngineBase;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
import org.xflatdb.xflat.transaction.Transaction;
import org.xflatdb.xflat.transaction.TransactionException;
import org.hamcrest.Matchers;
+import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -34,6 +34,11 @@
import static org.mockito.Mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import org.xflatdb.xflat.transaction.IllegalTransactionStateException;
+import org.xflatdb.xflat.transaction.Propagation;
+import org.xflatdb.xflat.transaction.TransactionOptions;
+import org.xflatdb.xflat.transaction.TransactionPropagationException;
+import org.xflatdb.xflat.transaction.TransactionScope;
/**
*
@@ -62,26 +67,28 @@ public void testBeginTransaction_TransactionIsOpen() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
+ Transaction txTransaction = instance.getTransaction();
+
- assertFalse("TX should not be committed", tx.isCommitted());
- assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(tx.getTransactionId()));
- assertFalse("TX should not be reverted", tx.isReverted());
- assertFalse("TX should not be reverted", instance.isTransactionReverted(tx.getTransactionId()));
+ assertFalse("TX should not be committed", scope.isCommitted());
+ assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(txTransaction.getTransactionId()));
+ assertFalse("TX should not be reverted", scope.isReverted());
+ assertFalse("TX should not be reverted", instance.isTransactionReverted(txTransaction.getTransactionId()));
assertTrue("Should be one open TX", instance.anyOpenTransactions());
- assertEquals(tx.getTransactionId(), instance.getLowestOpenTransaction());
+ assertEquals(txTransaction.getTransactionId(), instance.getLowestOpenTransaction());
- tx.commit();
+ scope.commit();
- assertTrue("TX should be committed", tx.isCommitted());
- assertEquals("TX should be committed", tx.getCommitId(), instance.isTransactionCommitted(tx.getTransactionId()));
- assertFalse("TX should not be reverted", tx.isReverted());
- assertFalse("TX should not be reverted", instance.isTransactionReverted(tx.getTransactionId()));
- assertThat("TX should have higher commit ID", tx.getCommitId(), Matchers.greaterThan(tx.getTransactionId()));
+ assertTrue("TX should be committed", scope.isCommitted());
+ assertEquals("TX should be committed", txTransaction.getCommitId(), instance.isTransactionCommitted(txTransaction.getTransactionId()));
+ assertFalse("TX should not be reverted", scope.isReverted());
+ assertFalse("TX should not be reverted", instance.isTransactionReverted(txTransaction.getTransactionId()));
+ assertThat("TX should have higher commit ID", txTransaction.getCommitId(), Matchers.greaterThan(txTransaction.getTransactionId()));
- tx.close();
+ scope.close();
assertFalse("Should be no open TX", instance.anyOpenTransactions());
assertEquals(Long.MAX_VALUE, instance.getLowestOpenTransaction());
@@ -96,17 +103,19 @@ public void testRevertTransaction_TransactionIsReverted() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
+ Transaction txTransaction = instance.getTransaction();
+
- tx.revert();
+ scope.revert();
- assertFalse("TX should not be committed", tx.isCommitted());
- assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(tx.getTransactionId()));
- assertTrue("TX should be reverted", tx.isReverted());
- assertTrue("TX should be reverted", instance.isTransactionReverted(tx.getTransactionId()));
- assertEquals("TX should have no commit ID", -1, tx.getCommitId());
+ assertFalse("TX should not be committed", scope.isCommitted());
+ assertEquals("TX should not be committed", -1, instance.isTransactionCommitted(txTransaction.getTransactionId()));
+ assertTrue("TX should be reverted", scope.isReverted());
+ assertTrue("TX should be reverted", instance.isTransactionReverted(txTransaction.getTransactionId()));
+ assertEquals("TX should have no commit ID", -1, txTransaction.getCommitId());
- tx.close();
+ scope.close();
assertFalse("Should be no open TX", instance.anyOpenTransactions());
assertEquals(Long.MAX_VALUE, instance.getLowestOpenTransaction());
@@ -120,13 +129,14 @@ public void testGetTransaction_GetsCurrentTransaction() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
-
+ TransactionScope scope = instance.openTransaction();
+
Transaction current = instance.getTransaction();
- assertEquals("Should be same transaction", tx.getTransactionId(), current.getTransactionId());
+ assertNotNull("should have current transaction", current);
+ assertThat("current should have an ID", current.getTransactionId(), Matchers.not(Matchers.equalTo(-1L)));
- tx.close();
+ scope.close();
assertNull("getTransaction should be null after closing", instance.getTransaction());
@@ -143,11 +153,12 @@ public void testTransactionlessCommitId_BeforeAndAfterTx_LTAndGTTxId() throws Ex
long id1 = instance.transactionlessCommitId();
- Transaction tx = instance.openTransaction();
-
+ TransactionScope scope = instance.openTransaction();
+ Transaction tx = instance.getTransaction();
+
long id2 = instance.transactionlessCommitId();
- tx.close();
+ scope.close();
assertThat(id1, Matchers.lessThan(tx.getTransactionId()));
assertThat(id2, Matchers.greaterThan(tx.getTransactionId()));
@@ -217,17 +228,17 @@ public void testBindEngine_BoundEngineNotifiedOfRevert() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
EngineBase e = mock(EngineBase.class);
instance.bindEngineToCurrentTransaction(e);
- tx.revert();
+ scope.revert();
- verify(e).revert(tx.getTransactionId(), false);
+ verify(e).revert(instance.getTransaction().getTransactionId(), false);
- tx.close();
+ scope.close();
}
}
@@ -238,17 +249,18 @@ public void testBindEngine_BoundEngineNotifiedOfCommit() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
+ Transaction tx = instance.getTransaction();
EngineBase e = mock(EngineBase.class);
instance.bindEngineToCurrentTransaction(e);
- tx.commit();
+ scope.commit();
- verify(e).commit(tx);
+ verify(e).commit(argThat(matchesTransaction(tx)), argThat(Matchers.equalTo(TransactionOptions.DEFAULT)));
- tx.close();
+ scope.close();
}
}
@@ -259,17 +271,18 @@ public void testBindEngine_ExceptionDuringCommit_BoundEngineReverted() throws Ex
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
+ Transaction tx = instance.getTransaction();
EngineBase e = mock(EngineBase.class);
doThrow(new TransactionException("Test"){})
- .when(e).commit(any(Transaction.class));
+ .when(e).commit(any(Transaction.class), any(TransactionOptions.class));
instance.bindEngineToCurrentTransaction(e);
try{
- tx.commit();
+ scope.commit();
fail("Did not throw TransactionException");
}
catch(TransactionException ex){
@@ -278,7 +291,7 @@ public void testBindEngine_ExceptionDuringCommit_BoundEngineReverted() throws Ex
verify(e).revert(tx.getTransactionId(), false);
- tx.close();
+ scope.close();
}
}
@@ -288,7 +301,8 @@ public void testMultipleBoundEngines_SecondEngineThrows_FirstBoundEngineReverted
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
+ TransactionScope scope = instance.openTransaction();
+ Transaction tx = instance.getTransaction();
final AtomicReference<EngineBase> committedEngine = new AtomicReference<>(null);
@@ -307,15 +321,15 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
EngineBase e = mock(EngineBase.class);
EngineBase e2 = mock(EngineBase.class);
doAnswer(a)
- .when(e).commit(any(Transaction.class));
+ .when(e).commit(any(Transaction.class), any(TransactionOptions.class));
doAnswer(a)
- .when(e2).commit(any(Transaction.class));
+ .when(e2).commit(any(Transaction.class), any(TransactionOptions.class));
instance.bindEngineToCurrentTransaction(e);
instance.bindEngineToCurrentTransaction(e2);
try{
- tx.commit();
+ scope.commit();
fail("Did not throw TransactionException");
}
catch(TransactionException ex){
@@ -325,9 +339,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
verify(e).revert(tx.getTransactionId(), false);
verify(e2).revert(tx.getTransactionId(), false);
- verify(committedEngine.get()).commit(tx);
+ verify(committedEngine.get()).commit(tx, TransactionOptions.DEFAULT);
- tx.close();
+ scope.close();
}
}
@@ -341,12 +355,14 @@ public void testBoundEngineFails_InstanceClosed_Recovers() throws Exception {
try(EngineTransactionManager instance = getInstance())
{
- Transaction tx = instance.openTransaction();
- txId = tx.getTransactionId();
+ TransactionScope tx = instance.openTransaction();
+ Transaction txTransaction = instance.getTransaction();
+
+ txId = txTransaction.getTransactionId();
EngineBase e = mock(EngineBase.class);
doThrow(new TransactionException("Test"){})
- .when(e).commit(any(Transaction.class));
+ .when(e).commit(any(Transaction.class), any(TransactionOptions.class));
doThrow(new Error("Expected"))
.when(e).revert(any(Long.class), anyBoolean());
doReturn("Name")
@@ -383,4 +399,413 @@ public void testBoundEngineFails_InstanceClosed_Recovers() throws Exception {
verify(e).revert(txId, true);
}
+ //<editor-fold desc="propagation">
+
+ @Test
+ public void testMandatoryPropagation_NoTransaction_ThrowsException() throws Exception {
+ System.out.println("testMandatoryPropagation_NoTransaction_ThrowsException");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try{
+
+ instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY));
+
+ fail("should have thrown TransactionPropagationException");
+ }catch(TransactionPropagationException ex){
+ //expected
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testMandatoryPropagation_TransactionExists_AmbientTransactionRequiresBothCommits() throws Exception {
+ System.out.println("testMandatoryPropagation_TransactionExists_AmbientTransactionRequiresBothCommits");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+
+ //act
+ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY));
+
+
+ inner.commit();
+
+ assertFalse("Ambient transaction should not yet be committed", inner.isCommitted());
+
+ inner.close();
+
+ //assert
+ assertFalse("Ambient transaction should not yet be committed", outer.isCommitted());
+
+ outer.commit();
+
+ assertTrue("Ambient transaction should be committed", inner.isCommitted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope() throws Exception {
+ System.out.println("testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+
+ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.MANDATORY));
+
+ //act
+ inner.close();
+
+ //assert
+ assertTrue("inner close should revert outer", outer.isReverted());
+
+ try{
+ outer.commit();
+ fail("should have thrown IllegalTransactionStateException on commit after revert");
+ }catch(IllegalTransactionStateException ex){
+ //expected
+ }
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testMandatoryPropagationReadOnly_TransactionExists_CloseDoesNotCloseOuterScope() throws Exception {
+ System.out.println("testMandatoryPropagation_TransactionExists_CloseDoesNotCloseOuterScope");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+
+ TransactionScope inner = instance.openTransaction(new TransactionOptions()
+ .withPropagation(Propagation.MANDATORY)
+ .withReadOnly(true));
+
+ //act
+ inner.close();
+
+ //assert
+ assertFalse("inner close should not revert outer", outer.isReverted());
+
+ outer.commit();
+ assertTrue("outer should still be capable of commit", outer.isCommitted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testNeverPropagation_TransactionExists_ThrowsException() throws Exception {
+ System.out.println("testNeverPropagation_TransactionExists_ThrowsException");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+
+ try{
+ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NEVER));
+
+ fail("should have thrown TransactionPropagationException");
+ }catch(TransactionPropagationException ex){
+ //expected
+ }
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testNeverPropagation_TransactionDoesNotExist_CurrentTransactionIsNull() throws Exception {
+ System.out.println("testNeverPropagation_TransactionDoesNotExist_CurrentTransactionIsNull");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+
+ TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NEVER));
+
+
+ Transaction current = instance.getTransaction();
+ assertNull("Current TX should be null", current);
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testNotSupportedPropagation_TransactionExists_ExistingIsSuspended() throws Exception {
+ System.out.println("testNotSupportedPropagation_TransactionExists_ExistingIsSuspended");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+ Transaction outerTx = instance.getTransaction();
+
+ //act
+ try(TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NOT_SUPPORTED))){
+
+
+ //assert
+ Transaction current = instance.getTransaction();
+ assertNull("Current TX should be null", current);
+
+ }
+
+ Transaction current = instance.getTransaction();
+ assertEquals("Should restore original transaction", outerTx.getCommitId(), current.getCommitId());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testNotSupportedPropagation_TransactionDoesntExist_OperatesNonTransactionally() throws Exception {
+ System.out.println("testNotSupportedPropagation_TransactionDoesntExist_OperatesNonTransactionally");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+
+ //act
+ try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.NOT_SUPPORTED))){
+ Transaction outerTx = instance.getTransaction();
+
+
+ assertNull("Current TX should be null", outerTx);
+
+ try(TransactionScope inner = instance.openTransaction()){
+
+ Transaction innerTx = instance.getTransaction();
+ assertNotNull("Should have inner transaction", innerTx);
+ assertThat("Should have inner transaction", innerTx.getTransactionId(), Matchers.greaterThan(0L));
+
+ inner.revert();
+ }
+
+ //assert
+ outerTx = instance.getTransaction();
+ assertNull("Current TX should still be null", outerTx);
+ assertFalse("Revert on inner should not propagate to outer", outer.isReverted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testRequiredPropagation_TransactionDoesNotExist_CreatesNewTransaction() throws Exception {
+ System.out.println("testRequiredPropagation_TransactionDoesNotExist_ThrowsException");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ //act
+ try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRED))){
+
+ Transaction current = instance.getTransaction();
+
+ assertNotNull("Current transaction should exist", current);
+ assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L));
+
+ outer.revert();
+
+ current = instance.getTransaction();
+ assertTrue("Current TX should be reverted", current.isReverted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testRequiredPropagation_TransactionExists_WrapsTransaction() throws Exception {
+ System.out.println("testRequiredPropagation_TransactionExists_WrapsTransaction");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+ Transaction outerTx = instance.getTransaction();
+
+ try (TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRED))) {
+
+ Transaction current = instance.getTransaction();
+
+ assertEquals("Current should be outer transaction", outerTx.getTransactionId(), current.getTransactionId());
+
+ inner.commit();
+ }
+
+ //assert
+ assertFalse("Commit should not yet have been finalized", outer.isCommitted());
+ outer.commit();
+ assertTrue("Commit should now be finalized", outer.isCommitted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testRequiresNewPropagation_NoTransactionExists_NewTransactionCreated() throws Exception {
+ System.out.println("testRequiresNewPropagation_NoTransactionExists_NewTransactionCreated");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+
+ //act
+ try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRES_NEW))){
+
+ Transaction current = instance.getTransaction();
+
+ assertNotNull("Current transaction should exist", current);
+ assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L));
+
+ outer.commit();
+
+ current = instance.getTransaction();
+ assertTrue("Current TX should be committed", current.isCommitted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testRequiresNewPropagation_TransactionExists_TransactionIsSuspended() throws Exception {
+ System.out.println("testRequiresNewPropagation_TransactionExists_TransactionIsSuspended");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+ Transaction outerTx = instance.getTransaction();
+
+ //act
+ try(TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.REQUIRES_NEW))){
+
+
+ //assert
+ Transaction current = instance.getTransaction();
+ assertNotNull("Current transaction should exist", current);
+ assertThat("Should have TX ID", current.getTransactionId(), Matchers.greaterThan(0L));
+ assertThat("Inner TX should not be outer", current.getTransactionId(), Matchers.not(Matchers.equalTo(outerTx.getTransactionId())));
+
+ inner.revert();
+ }
+
+ Transaction current = instance.getTransaction();
+ assertEquals("Should restore original transaction", outerTx.getTransactionId(), current.getTransactionId());
+ assertFalse("revert on inner should propagate to outer", outer.isReverted());
+ assertFalse("revert on inner should propagate to outer", current.isReverted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testSupportsPropagation_TransactionDoesNotExist_ExecutesNonTransactionally() throws Exception {
+ System.out.println("testSupportsPropagation_TransactionDoesNotExist_ExecutesNonTransactionally");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+
+ //act
+ try(TransactionScope outer = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.SUPPORTS))){
+
+ //assert
+ Transaction current = instance.getTransaction();
+ assertNull("Current TX should be null", current);
+
+ try(TransactionScope inner = instance.openTransaction()){
+
+ current = instance.getTransaction();
+ assertNotNull("Should now have inner transaction", current);
+
+ inner.revert();
+ }
+
+ //assert
+ current = instance.getTransaction();
+ assertNull("Current TX should be null", current);
+ assertFalse("Revert on inner should not propagate to outer", outer.isReverted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+ @Test
+ public void testSupportsPropagation_TransactionExists_WrapsCurrentTransaction() throws Exception {
+ System.out.println("testSupportsPropagation_TransactionExists_WrapsCurrentTransaction");
+
+ try(EngineTransactionManager instance = getInstance())
+ {
+ try(TransactionScope outer = instance.openTransaction()){
+ Transaction outerTx = instance.getTransaction();
+
+ try (TransactionScope inner = instance.openTransaction(new TransactionOptions().withPropagation(Propagation.SUPPORTS))) {
+
+ Transaction current = instance.getTransaction();
+
+ assertEquals("Current should be outer transaction", outerTx.getTransactionId(), current.getTransactionId());
+
+ inner.commit();
+ }
+
+ //assert
+ assertFalse("Commit should have not yet been finalized", outer.isCommitted());
+ Transaction current = instance.getTransaction();
+ assertEquals("current should be outer TX", outerTx.getTransactionId(), current.getTransactionId());
+
+ outer.commit();
+ assertTrue("Commit should now be