Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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 finalized", outer.isCommitted());
+ }
+
+ assertFalse("Should not be any more open transactions", instance.anyOpenTransactions());
+ }
+ }
+
+