You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2018/03/27 10:06:34 UTC

cayenne git commit: CAY-2415 Transaction isolation and propagation support

Repository: cayenne
Updated Branches:
  refs/heads/master 23c3a8c5d -> 8c8a2f0be


CAY-2415 Transaction isolation and propagation support


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/8c8a2f0b
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/8c8a2f0b
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/8c8a2f0b

Branch: refs/heads/master
Commit: 8c8a2f0bea2679cab65d9a1de05d5a72e883dcb7
Parents: 23c3a8c
Author: Nikita Timofeev <st...@gmail.com>
Authored: Tue Mar 27 13:06:22 2018 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Tue Mar 27 13:06:22 2018 +0300

----------------------------------------------------------------------
 RELEASE-NOTES.txt                               |   1 +
 .../org/apache/cayenne/tx/BaseTransaction.java  |  25 ++-
 .../apache/cayenne/tx/CayenneTransaction.java   |  11 +-
 .../tx/DefaultTransactionDescriptor.java        |  36 ++++
 .../cayenne/tx/DefaultTransactionFactory.java   |  12 +-
 .../cayenne/tx/DefaultTransactionManager.java   | 168 +++++++++++++++----
 .../apache/cayenne/tx/ExternalTransaction.java  |   8 +
 .../cayenne/tx/TransactionDescriptor.java       |  93 ++++++++++
 .../apache/cayenne/tx/TransactionFactory.java   |   9 +
 .../apache/cayenne/tx/TransactionManager.java   |  30 ++++
 .../cayenne/tx/TransactionPropagation.java      |  41 +++++
 .../cayenne/tx/TransactionalOperation.java      |   1 +
 .../configuration/server/ServerRuntimeTest.java |   3 +
 .../cayenne/tx/DefaultTransactionManagerIT.java | 151 ++++++++++++++---
 .../cayenne/tx/TransactionIsolationIT.java      | 137 +++++++++++++++
 .../tx/TransactionPropagationRollbackIT.java    | 159 ++++++++++++++++++
 .../cayenne/unit/OracleUnitDbAdapter.java       |   5 +
 .../cayenne/unit/PostgresUnitDbAdapter.java     |   5 +
 .../org/apache/cayenne/unit/UnitDbAdapter.java  |   4 +
 19 files changed, 838 insertions(+), 61 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 5228c96..0ce5f8d 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -27,6 +27,7 @@ CAY-2406 Add prefetch-related API to SQLSelect
 CAY-2407 Modeler: add prefetch support for the SQLTemplate query
 CAY-2410 Add prefetch type support for SQLTemplate query and SelectQuery
 CAY-2414 Modeler: new icon design
+CAY-2415 Transaction isolation and propagation support
 
 Bug Fixes:
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
index 9b7ac0f..5c9793e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
@@ -28,6 +28,8 @@ import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 
+import org.apache.cayenne.CayenneRuntimeException;
+
 /**
  * A Cayenne transaction. Currently supports managing JDBC connections.
  *
@@ -51,6 +53,8 @@ public abstract class BaseTransaction implements Transaction {
     protected Map<String, Connection> connections;
     protected Collection<TransactionListener> listeners;
     protected int status;
+    protected int defaultIsolationLevel = -1;
+    protected TransactionDescriptor descriptor;
 
     static String decodeStatus(int status) {
         switch (status) {
@@ -91,8 +95,9 @@ public abstract class BaseTransaction implements Transaction {
     /**
      * Creates new inactive transaction.
      */
-    protected BaseTransaction() {
+    protected BaseTransaction(TransactionDescriptor descriptor) {
         this.status = STATUS_NO_TRANSACTION;
+        this.descriptor = descriptor;
     }
 
     @Override
@@ -214,6 +219,15 @@ public abstract class BaseTransaction implements Transaction {
 
     protected Connection addConnection(String connectionName, Connection connection) {
 
+        if(descriptor.getIsolation() != TransactionDescriptor.ISOLATION_DEFAULT) {
+            try {
+                defaultIsolationLevel = connection.getTransactionIsolation();
+                connection.setTransactionIsolation(descriptor.getIsolation());
+            } catch (SQLException ex) {
+                throw new CayenneRuntimeException("Unable to set required isolation level: " + descriptor.getIsolation(), ex);
+            }
+        }
+
         TransactionConnectionDecorator wrapper = new TransactionConnectionDecorator(connection);
 
         if (listeners != null) {
@@ -262,6 +276,15 @@ public abstract class BaseTransaction implements Transaction {
             } catch (Throwable th) {
                 // TODO: chain exceptions...
                 // ignore for now
+            } finally {
+                // restore connection default isolation level ...
+                if(defaultIsolationLevel != -1) {
+                    try {
+                        c.setTransactionIsolation(defaultIsolationLevel);
+                    } catch (SQLException ignore) {
+                        // have no meaningful options here...
+                    }
+                }
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
index 5554f07..9115b17 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
@@ -35,7 +35,15 @@ public class CayenneTransaction extends BaseTransaction {
     protected JdbcEventLogger logger;
 
     public CayenneTransaction(JdbcEventLogger logger) {
-        this.logger = logger;
+        this(logger, DefaultTransactionDescriptor.getInstance());
+    }
+
+    /**
+     * @since 4.1
+     */
+    public CayenneTransaction(JdbcEventLogger jdbcEventLogger, TransactionDescriptor descriptor) {
+        super(descriptor);
+        this.logger = jdbcEventLogger;
     }
 
     @Override
@@ -127,6 +135,7 @@ public class CayenneTransaction extends BaseTransaction {
             }
         }
 
+        logger.logRollbackTransaction("transaction rolledback.");
         if (deferredException != null) {
             throw new CayenneRuntimeException(deferredException);
         }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionDescriptor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionDescriptor.java
new file mode 100644
index 0000000..bad8e3c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionDescriptor.java
@@ -0,0 +1,36 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.tx;
+
+/**
+ * @since 4.1
+ */
+public class DefaultTransactionDescriptor extends TransactionDescriptor {
+
+    private static final DefaultTransactionDescriptor instance = new DefaultTransactionDescriptor();
+
+    public static TransactionDescriptor getInstance() {
+        return instance;
+    }
+
+    private DefaultTransactionDescriptor() {
+        super(TransactionDescriptor.ISOLATION_DEFAULT, TransactionPropagation.NESTED);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
index 8687204..116f7d7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
@@ -39,8 +39,16 @@ public class DefaultTransactionFactory implements TransactionFactory {
 
     @Override
     public Transaction createTransaction() {
-        return externalTransactions ? new ExternalTransaction(jdbcEventLogger) : new CayenneTransaction(
-                jdbcEventLogger);
+        return createTransaction(DefaultTransactionDescriptor.getInstance());
+    }
+
+    /**
+     * @since 4.1
+     */
+    @Override
+    public Transaction createTransaction(TransactionDescriptor descriptor) {
+        return externalTransactions ? new ExternalTransaction(jdbcEventLogger, descriptor) : new CayenneTransaction(
+                jdbcEventLogger, descriptor);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
index 61ed4b6..e239af7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
@@ -18,6 +18,9 @@
  ****************************************************************/
 package org.apache.cayenne.tx;
 
+import java.sql.Connection;
+import java.sql.SQLException;
+
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -37,54 +40,149 @@ public class DefaultTransactionManager implements TransactionManager {
 
     @Override
     public <T> T performInTransaction(TransactionalOperation<T> op) {
-        return performInTransaction(op, DoNothingTransactionListener.getInstance());
+        return performInTransaction(op, DoNothingTransactionListener.getInstance(), DefaultTransactionDescriptor.getInstance());
     }
 
     @Override
     public <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback) {
+        return performInTransaction(op, callback, DefaultTransactionDescriptor.getInstance());
+    }
+
+    /**
+     * @since 4.1
+     */
+    @Override
+    public <T> T performInTransaction(TransactionalOperation<T> op, TransactionDescriptor descriptor) {
+        return performInTransaction(op, DoNothingTransactionListener.getInstance(), descriptor);
+    }
 
-        // Either join existing tx (in such case do not try to commit or rollback), or start a new tx and manage it
-        // till the end
+    /**
+     * @since 4.1
+     */
+    @Override
+    public <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor) {
+        BaseTransactionHandler handler = getHandler(descriptor);
+        return handler.handle(op, callback, descriptor);
+    }
+
+    protected BaseTransactionHandler getHandler(TransactionDescriptor descriptor) {
+        switch (descriptor.getPropagation()) {
+            // MANDATORY requires transaction to exists
+            case MANDATORY:
+                return new MandatoryTransactionHandler(txFactory, jdbcEventLogger);
+
+            // NESTED can join existing or create new
+            case NESTED:
+                return new NestedTransactionHandler(txFactory, jdbcEventLogger);
+
+            // REQUIRES_NEW should always create new transaction
+            case REQUIRES_NEW:
+                return new RequiresNewTransactionHandler(txFactory, jdbcEventLogger);
+        }
 
-        Transaction currentTx = BaseTransaction.getThreadTransaction();
-        return (currentTx != null)
-                ? performInTransaction(currentTx, op, callback)
-                : performInLocalTransaction(op, callback);
+        throw new CayenneRuntimeException("Unsupported transaction propagation: " + descriptor.getPropagation());
     }
 
-    protected <T> T performInLocalTransaction(TransactionalOperation<T> op, TransactionListener callback) {
-        Transaction tx = txFactory.createTransaction();
-        BaseTransaction.bindThreadTransaction(tx);
-        try {
-            T result = performInTransaction(tx, op, callback);
-            tx.commit();
-            return result;
-
-        } catch (CayenneRuntimeException ex) {
-            tx.setRollbackOnly();
-            throw ex;
-        } catch (Exception ex) {
-            tx.setRollbackOnly();
-            throw new CayenneRuntimeException(ex);
-        } finally {
-            BaseTransaction.bindThreadTransaction(null);
-
-            if (tx.isRollbackOnly()) {
-                try {
-                    tx.rollback();
-                } catch (Exception e) {
-                    // although we don't expect an exception here, print the
-                    // stack, as there have been some Cayenne bugs already
-                    // (CAY-557) that were masked by this 'catch' clause.
-                    jdbcEventLogger.logQueryError(e);
+    private static class NestedTransactionHandler extends BaseTransactionHandler {
+
+        private NestedTransactionHandler(TransactionFactory txFactory, JdbcEventLogger jdbcEventLogger) {
+            super(txFactory, jdbcEventLogger);
+        }
+
+        @Override
+        protected <T> T handle(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor) {
+            Transaction currentTx = BaseTransaction.getThreadTransaction();
+            if(currentTx != null) {
+                return performInTransaction(currentTx, op, callback);
+            } else {
+                return performInNewTransaction(op, callback, descriptor);
+            }
+        }
+    }
+
+    private static class MandatoryTransactionHandler extends BaseTransactionHandler {
+
+        private MandatoryTransactionHandler(TransactionFactory txFactory, JdbcEventLogger jdbcEventLogger) {
+            super(txFactory, jdbcEventLogger);
+        }
+
+        @Override
+        protected <T> T handle(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor) {
+            Transaction currentTx = BaseTransaction.getThreadTransaction();
+            if(currentTx == null) {
+                throw new CayenneRuntimeException("Transaction operation should join to existing transaction but none found.");
+            }
+            return performInTransaction(currentTx, op, callback);
+        }
+    }
+
+    private static class RequiresNewTransactionHandler extends BaseTransactionHandler {
+
+        private RequiresNewTransactionHandler(TransactionFactory txFactory, JdbcEventLogger jdbcEventLogger) {
+            super(txFactory, jdbcEventLogger);
+        }
+
+        @Override
+        protected <T> T handle(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor) {
+            Transaction currentTx = BaseTransaction.getThreadTransaction();
+            try {
+                return performInNewTransaction(op, callback, descriptor);
+            } finally {
+                if(currentTx != null) {
+                    // restore old transaction, if where set
+                    BaseTransaction.bindThreadTransaction(currentTx);
                 }
             }
         }
     }
 
-    protected <T> T performInTransaction(Transaction tx, TransactionalOperation<T> op, TransactionListener callback) {
-        tx.addListener(callback);
-        return op.perform();
+    protected static abstract class BaseTransactionHandler {
+
+        private TransactionFactory txFactory;
+        private JdbcEventLogger jdbcEventLogger;
+
+        private BaseTransactionHandler(TransactionFactory txFactory, JdbcEventLogger jdbcEventLogger) {
+            this.txFactory = txFactory;
+            this.jdbcEventLogger = jdbcEventLogger;
+        }
+
+        protected abstract <T> T handle(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor);
+
+        protected <T> T performInNewTransaction(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor) {
+            Transaction tx = txFactory.createTransaction(descriptor);
+            BaseTransaction.bindThreadTransaction(tx);
+            try {
+                T result = performInTransaction(tx, op, callback);
+                tx.commit();
+                return result;
+
+            } catch (CayenneRuntimeException ex) {
+                tx.setRollbackOnly();
+                throw ex;
+            } catch (Exception ex) {
+                tx.setRollbackOnly();
+                throw new CayenneRuntimeException(ex);
+            } finally {
+                BaseTransaction.bindThreadTransaction(null);
+
+                if (tx.isRollbackOnly()) {
+                    try {
+                        tx.rollback();
+                    } catch (Exception e) {
+                        // although we don't expect an exception here, print the
+                        // stack, as there have been some Cayenne bugs already
+                        // (CAY-557) that were masked by this 'catch' clause.
+                        jdbcEventLogger.logQueryError(e);
+                    }
+                }
+            }
+        }
+
+        protected <T> T performInTransaction(Transaction tx, TransactionalOperation<T> op, TransactionListener callback) {
+            tx.addListener(callback);
+            return op.perform();
+        }
+
     }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/ExternalTransaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/ExternalTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/ExternalTransaction.java
index 8afa94f..18b6f92 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/ExternalTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/ExternalTransaction.java
@@ -31,6 +31,14 @@ public class ExternalTransaction extends BaseTransaction {
     protected JdbcEventLogger logger;
 
     public ExternalTransaction(JdbcEventLogger jdbcEventLogger) {
+        this(jdbcEventLogger, DefaultTransactionDescriptor.getInstance());
+    }
+
+    /**
+     * @since 4.1
+     */
+    public ExternalTransaction(JdbcEventLogger jdbcEventLogger, TransactionDescriptor descriptor) {
+        super(descriptor);
         this.logger = jdbcEventLogger;
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
new file mode 100644
index 0000000..7c29700
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
@@ -0,0 +1,93 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.tx;
+
+/**
+ *
+ * Descriptor that provide desired transaction isolation level and propagation logic.
+ *
+ * @since 4.1
+ */
+public class TransactionDescriptor {
+
+    /**
+     * Keep database default isolation level
+     */
+    public static final int ISOLATION_DEFAULT = -1;
+
+    private final int isolation;
+
+    private final TransactionPropagation propagation;
+
+    /**
+     * @param isolation one of the following <code>Connection</code> constants:
+     *        <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
+     *        <code>Connection.TRANSACTION_READ_COMMITTED</code>,
+     *        <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
+     *        <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
+     *        <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
+     *
+     * @param propagation transaction propagation behaviour
+     *
+     * @see TransactionPropagation
+     */
+    public TransactionDescriptor(int isolation, TransactionPropagation propagation) {
+        this.isolation = isolation;
+        this.propagation = propagation;
+    }
+
+    /**
+     *
+     * Create transaction descriptor with desired isolation level and <code>NESTED</code> propagation
+     *
+     * @param isolation one of the following <code>Connection</code> constants:
+     *        <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
+     *        <code>Connection.TRANSACTION_READ_COMMITTED</code>,
+     *        <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
+     *        <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
+     *        <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
+     */
+    public TransactionDescriptor(int isolation) {
+        this(isolation, TransactionPropagation.NESTED);
+    }
+
+    /**
+     *
+     * @param propagation transaction propagation behaviour
+     * @see TransactionPropagation
+     */
+    public TransactionDescriptor(TransactionPropagation propagation) {
+        this(ISOLATION_DEFAULT, propagation);
+    }
+
+    /**
+     * @return required isolation level
+     */
+    public int getIsolation() {
+        return isolation;
+    }
+
+    /**
+     * @return required propagation behaviour
+     */
+    public TransactionPropagation getPropagation() {
+        return propagation;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
index 6c31eb5..a01be38 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
@@ -25,4 +25,13 @@ package org.apache.cayenne.tx;
 public interface TransactionFactory {
 
     Transaction createTransaction();
+
+    /**
+     *
+     * @param descriptor with required transaction properties
+     * @return new transaction
+     *
+     * @since 4.1
+     */
+    Transaction createTransaction(TransactionDescriptor descriptor);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java
index cb17c8f..c058850 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java
@@ -33,6 +33,7 @@ public interface TransactionManager {
      * transaction.
      *
      * @param op an operation to perform within the transaction.
+     * @param <T> returned value type
      * @return a value returned by the "op" operation.
      */
     <T> T performInTransaction(TransactionalOperation<T> op);
@@ -45,7 +46,36 @@ public interface TransactionManager {
      *
      * @param op       an operation to perform within the transaction.
      * @param callback a callback to notify as transaction progresses through stages.
+     * @param <T> returned value type
      * @return a value returned by the "op" operation.
      */
     <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback);
+
+
+    /**
+     * Performs operation in a transaction which parameters described by descriptor.
+     *
+     * @param op         an operation to perform within the transaction.
+     * @param descriptor transaction descriptor
+     * @param <T> result type
+     * @return a value returned by the "op" operation.
+     *
+     * @since 4.1
+     */
+    <T> T performInTransaction(TransactionalOperation<T> op, TransactionDescriptor descriptor);
+
+    /**
+     * Performs operation in a transaction which parameters described by descriptor.
+     * As transaction goes through stages, callback methods are invoked allowing the caller to customize
+     * transaction parameters.
+     *
+     * @param op         an operation to perform within the transaction.
+     * @param callback   a callback to notify as transaction progresses through stages.
+     * @param descriptor transaction descriptor
+     * @param <T> returned value type
+     * @return a value returned by the "op" operation.
+     *
+     * @since 4.1
+     */
+    <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback, TransactionDescriptor descriptor);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
new file mode 100644
index 0000000..5aea027
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
@@ -0,0 +1,41 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.tx;
+
+/**
+ * Propagation behaviour of transaction
+ */
+public enum TransactionPropagation {
+    /**
+     * Support a current transaction, throw an exception if none exists.
+     */
+    MANDATORY,
+
+    /**
+     * Execute within a nested transaction if a current transaction exists,
+     * create a new one if none exists.
+     */
+    NESTED,
+
+    /**
+     * Create a new transaction, and suspend the current transaction if one exists.
+     */
+    REQUIRES_NEW
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionalOperation.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionalOperation.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionalOperation.java
index 94f519e..a698d51 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionalOperation.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionalOperation.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.tx;
 /**
  * @since 4.0
  */
+@FunctionalInterface
 public interface TransactionalOperation<T> {
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeTest.java b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeTest.java
index c4c4ce8..98df8bd 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeTest.java
@@ -31,6 +31,7 @@ import org.apache.cayenne.graph.GraphDiff;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.tx.BaseTransaction;
+import org.apache.cayenne.tx.TransactionDescriptor;
 import org.apache.cayenne.tx.TransactionFactory;
 import org.apache.cayenne.tx.TransactionalOperation;
 import org.junit.Test;
@@ -42,6 +43,7 @@ import java.util.List;
 
 import static java.util.Arrays.asList;
 import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -53,6 +55,7 @@ public class ServerRuntimeTest {
         final BaseTransaction tx = mock(BaseTransaction.class);
         final TransactionFactory txFactory = mock(TransactionFactory.class);
         when(txFactory.createTransaction()).thenReturn(tx);
+        when(txFactory.createTransaction(any(TransactionDescriptor.class))).thenReturn(tx);
 
         Module module = binder -> binder.bind(TransactionFactory.class).toInstance(txFactory);
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java
index 6b9f001..0c006d0 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java
@@ -18,35 +18,32 @@
  ****************************************************************/
 package org.apache.cayenne.tx;
 
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class DefaultTransactionManagerIT extends ServerCase {
+public class DefaultTransactionManagerIT {
 
     @Test
     public void testPerformInTransaction_Local() {
 
         final BaseTransaction tx = mock(BaseTransaction.class);
-        TransactionFactory txFactory = mock(TransactionFactory.class);
-        when(txFactory.createTransaction()).thenReturn(tx);
 
-        DefaultTransactionManager txManager = new DefaultTransactionManager(txFactory, mock(JdbcEventLogger.class));
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> tx);
 
         final Object expectedResult = new Object();
-        Object result = txManager.performInTransaction(new TransactionalOperation<Object>() {
-            public Object perform() {
-                assertNotNull(BaseTransaction.getThreadTransaction());
-                return expectedResult;
-            }
+        Object result = txManager.performInTransaction(() -> {
+            assertSame(tx, BaseTransaction.getThreadTransaction());
+            return expectedResult;
         });
 
         assertSame(expectedResult, result);
@@ -56,21 +53,17 @@ public class DefaultTransactionManagerIT extends ServerCase {
     public void testPerformInTransaction_ExistingTx() {
 
         final BaseTransaction tx1 = mock(BaseTransaction.class);
-        TransactionFactory txFactory = mock(TransactionFactory.class);
-        when(txFactory.createTransaction()).thenReturn(tx1);
 
-        DefaultTransactionManager txManager = new DefaultTransactionManager(txFactory, mock(JdbcEventLogger.class));
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> tx1);
 
         final BaseTransaction tx2 = mock(BaseTransaction.class);
         BaseTransaction.bindThreadTransaction(tx2);
         try {
 
             final Object expectedResult = new Object();
-            Object result = txManager.performInTransaction(new TransactionalOperation<Object>() {
-                public Object perform() {
-                    assertSame(tx2, BaseTransaction.getThreadTransaction());
-                    return expectedResult;
-                }
+            Object result = txManager.performInTransaction(() -> {
+                assertSame(tx2, BaseTransaction.getThreadTransaction());
+                return expectedResult;
             });
 
             assertSame(expectedResult, result);
@@ -79,5 +72,119 @@ public class DefaultTransactionManagerIT extends ServerCase {
         }
     }
 
+    @Test
+    public void testNestedPropagation() {
+        final BaseTransaction tx = mock(BaseTransaction.class);
+
+        assertNull(BaseTransaction.getThreadTransaction());
+
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> tx);
+
+        try {
+            final Object expectedResult = new Object();
+            Object result = txManager.performInTransaction(() -> {
+                    assertSame(tx, BaseTransaction.getThreadTransaction());
+                    return expectedResult;
+                },
+                new TransactionDescriptor(TransactionPropagation.NESTED)
+            );
+            assertSame(expectedResult, result);
+        } finally {
+            BaseTransaction.bindThreadTransaction(null);
+        }
+
+    }
+
+    @Test(expected = CayenneRuntimeException.class)
+    public void testMandatoryPropagationNotStarted() {
+        final BaseTransaction tx = mock(BaseTransaction.class);
+
+        assertNull(BaseTransaction.getThreadTransaction());
+
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> tx);
+
+        try {
+            final Object expectedResult = new Object();
+            Object result = txManager.performInTransaction(() -> {
+                        assertSame(tx, BaseTransaction.getThreadTransaction());
+                        return expectedResult;
+                    },
+                    new TransactionDescriptor(TransactionPropagation.MANDATORY)
+            );
+            assertSame(expectedResult, result);
+        } finally {
+            BaseTransaction.bindThreadTransaction(null);
+        }
+
+    }
+
+    @Test
+    public void testMandatoryPropagation() {
+        final BaseTransaction tx = mock(BaseTransaction.class);
+
+        assertNull(BaseTransaction.getThreadTransaction());
+
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> tx);
+        BaseTransaction.bindThreadTransaction(tx);
+
+        try {
+            final Object expectedResult = new Object();
+            Object result = txManager.performInTransaction(() -> {
+                        assertSame(tx, BaseTransaction.getThreadTransaction());
+                        return expectedResult;
+                    },
+                    new TransactionDescriptor(TransactionPropagation.MANDATORY)
+            );
+            assertSame(expectedResult, result);
+        } finally {
+            BaseTransaction.bindThreadTransaction(null);
+        }
+
+    }
+
+    @Test
+    public void testRequiresNewPropagation() {
+        final BaseTransaction tx1 = mock(BaseTransaction.class);
+        final BaseTransaction tx2 = mock(BaseTransaction.class);
+        final AtomicInteger counter = new AtomicInteger(0);
+
+        assertNull(BaseTransaction.getThreadTransaction());
+
+        DefaultTransactionManager txManager = createDefaultTxManager(() -> {
+            counter.incrementAndGet();
+            return tx2;
+        });
+
+        BaseTransaction.bindThreadTransaction(tx1);
+
+        try {
+            final Object expectedResult = new Object();
+            Object result = txManager.performInTransaction(() -> {
+                        assertSame(tx2, BaseTransaction.getThreadTransaction());
+                        return expectedResult;
+                    },
+                    new TransactionDescriptor(TransactionPropagation.REQUIRES_NEW)
+            );
+            assertSame(expectedResult, result);
+            assertSame(tx1, BaseTransaction.getThreadTransaction());
+        } finally {
+            BaseTransaction.bindThreadTransaction(null);
+        }
+
+    }
+
+    private DefaultTransactionManager createDefaultTxManager(final Supplier<Transaction> txSupplier) {
+        return new DefaultTransactionManager(
+                createMockFactory(txSupplier),
+                mock(JdbcEventLogger.class)
+        );
+    }
+
+    private TransactionFactory createMockFactory(final Supplier<Transaction> supplier) {
+        TransactionFactory txFactory = mock(TransactionFactory.class);
+        when(txFactory.createTransaction()).thenReturn(supplier.get());
+        when(txFactory.createTransaction(any(TransactionDescriptor.class))).thenReturn(supplier.get());
+        return txFactory;
+    }
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionIsolationIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionIsolationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionIsolationIT.java
new file mode 100644
index 0000000..2671352
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionIsolationIT.java
@@ -0,0 +1,137 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.tx;
+
+import java.sql.Connection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.1
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class TransactionIsolationIT extends ServerCase {
+
+    private final Logger logger = LoggerFactory.getLogger(TransactionIsolationIT.class);
+
+    @Inject
+    DataContext context;
+
+    @Inject
+    ServerRuntime runtime;
+
+    @Inject
+    UnitDbAdapter unitDbAdapter;
+
+    TransactionManager manager;
+
+    @Before
+    public void initTransactionManager() {
+        // no binding in test container, get it from runtime
+        manager = runtime.getInjector().getInstance(TransactionManager.class);
+    }
+
+    @Test
+    public void testIsolationLevel() throws Exception {
+
+        if(!unitDbAdapter.supportsSerializableTransactionIsolation()) {
+            return;
+        }
+
+        TransactionDescriptor descriptor = new TransactionDescriptor(
+                Connection.TRANSACTION_REPEATABLE_READ,
+                TransactionPropagation.REQUIRES_NEW
+        );
+
+        CountDownLatch startSignal = new CountDownLatch(1);
+        CountDownLatch resumeSerializableTransaction = new CountDownLatch(1);
+        ExecutorService service = Executors.newFixedThreadPool(2);
+
+        Future<Boolean> thread1Result = service.submit(() -> {
+            try {
+                return manager.performInTransaction(() -> {
+                    long result;
+                    try {
+                        result = ObjectSelect.query(Artist.class).selectCount(context);
+                    } finally {
+                        startSignal.countDown();
+                    }
+                    if(result != 0) {
+                        logger.error("First fetch returned " + result);
+                        return false;
+                    }
+                    try {
+                        resumeSerializableTransaction.await();
+                    } catch (InterruptedException e) {
+                        logger.error("Resume signal await failed", e);
+                        return false;
+                    }
+
+                    result = ObjectSelect.query(Artist.class).selectCount(context);
+                    logger.info("Second fetch returned " + result);
+                    return result == 0;
+                }, descriptor);
+            } catch (Exception ex) {
+                logger.error("Perform in transaction failed", ex);
+                return false;
+            }
+        });
+
+        Future<Boolean> thread2Result = service.submit(() -> {
+            try {
+                startSignal.await();
+                try {
+                    Artist artist = context.newObject(Artist.class);
+                    artist.setArtistName("artist");
+                    context.commitChanges();
+                } finally {
+                    resumeSerializableTransaction.countDown();
+                }
+            } catch (Exception ex) {
+                logger.error("Unable to create Artist", ex);
+                return false;
+            }
+            return true;
+        });
+
+        assertTrue(thread1Result.get(30, TimeUnit.SECONDS));
+        assertTrue(thread2Result.get(30, TimeUnit.SECONDS));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionPropagationRollbackIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionPropagationRollbackIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionPropagationRollbackIT.java
new file mode 100644
index 0000000..ae94b8a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionPropagationRollbackIT.java
@@ -0,0 +1,159 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.tx;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Connection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ *
+ * This test checks rollback behaviour of different propagation modes.
+ *
+ * @see TransactionPropagation
+ * @since 4.1
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class TransactionPropagationRollbackIT extends ServerCase {
+
+    @Inject
+    DataContext context;
+
+    @Inject
+    ServerRuntime runtime;
+
+    @Inject
+    UnitDbAdapter unitDbAdapter;
+
+    TransactionManager manager;
+
+    @Before
+    public void initTransactionManager() {
+        // no binding in test container, get it from runtime
+        manager = runtime.getInjector().getInstance(TransactionManager.class);
+    }
+
+    /**
+     * @see TransactionPropagation#REQUIRES_NEW
+     */
+    @Test
+    public void testPropagationRequiresNew() {
+        TransactionDescriptor descriptor = new TransactionDescriptor(
+                Connection.TRANSACTION_SERIALIZABLE, // ensure that transaction not visible to each other
+                TransactionPropagation.REQUIRES_NEW  // require new transaction for every operation
+        );
+
+        performInTransaction(descriptor);
+
+        // rollback should be performed and no artist will be in DB
+        assertEquals(0L, ObjectSelect.query(Artist.class).selectCount(context));
+
+        // painting should be there
+        assertEquals(1L, ObjectSelect.query(Painting.class).selectCount(context));
+    }
+
+    /**
+     * @see TransactionPropagation#NESTED
+     */
+    @Test
+    public void testPropagationNested() {
+
+        TransactionDescriptor descriptor = new TransactionDescriptor(
+                Connection.TRANSACTION_SERIALIZABLE, // ensure that transaction not visible to each other
+                TransactionPropagation.NESTED        // allow joining to existing transaction
+        );
+
+        performInTransaction(descriptor);
+
+        // nested rollback shouldn't affect outer transaction
+        assertEquals(1L, ObjectSelect.query(Artist.class).selectCount(context));
+
+        // painting should be there
+        assertEquals(1L, ObjectSelect.query(Painting.class).selectCount(context));
+    }
+
+    /**
+     * @see TransactionPropagation#MANDATORY
+     */
+    @Test
+    public void testPropagationMandatory() {
+
+        TransactionDescriptor descriptor = new TransactionDescriptor(
+                Connection.TRANSACTION_SERIALIZABLE, // ensure that transaction not visible to each other
+                TransactionPropagation.MANDATORY     // requires existing transaction to join
+        );
+
+        performInTransaction(descriptor);
+
+        // nested rollback shouldn't affect outer transaction
+        assertEquals(1L, ObjectSelect.query(Artist.class).selectCount(context));
+
+        // painting should be there
+        assertEquals(1L, ObjectSelect.query(Painting.class).selectCount(context));
+    }
+
+    private void performInTransaction(TransactionDescriptor descriptor) {
+        Artist artist = context.newObject(Artist.class);
+        artist.setArtistName("test");
+
+        manager.performInTransaction(() -> {
+            // try to perform illegal operation in nested transaction
+            try {
+                manager.performInTransaction(() -> {
+                    artist.setArtistName("test3");
+                    context.commitChanges(); // this should pass
+
+                    artist.setArtistName(null);
+                    context.commitChanges(); // this should throw
+                    return null;
+                }, descriptor);
+                fail("Exception should be thrown");
+            } catch (Exception ignore) {
+            }
+
+            // perform some valid commit
+            artist.setArtistName("test2");
+
+            Painting painting = context.newObject(Painting.class);
+            painting.setPaintingTitle("painting");
+
+            // Outcome of this will depend on transaction propagation
+            // if it's nested or mandatory we'll have here artist committed,
+            // if it's new no artist should be in database
+            context.commitChanges();
+            return null;
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/unit/OracleUnitDbAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/OracleUnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/OracleUnitDbAdapter.java
index 633a03d..a6b381a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/OracleUnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/OracleUnitDbAdapter.java
@@ -145,4 +145,9 @@ public class OracleUnitDbAdapter extends UnitDbAdapter {
     public boolean supportsSelectBooleanExpression() {
         return false;
     }
+
+    @Override
+    public boolean supportsSerializableTransactionIsolation() {
+        return true;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
index 1843bc2..d9d6383 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
@@ -70,4 +70,9 @@ public class PostgresUnitDbAdapter extends UnitDbAdapter {
     public boolean supportsGeneratedKeysDrop() {
         return true;
     }
+
+    @Override
+    public boolean supportsSerializableTransactionIsolation() {
+        return true;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8c8a2f0b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
index 0a83243..54e68a8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
@@ -403,4 +403,8 @@ public class UnitDbAdapter {
     public boolean supportsExtractPart(ASTExtract.DateTimePart part) {
         return true;
     }
+
+    public boolean supportsSerializableTransactionIsolation() {
+        return false;
+    }
 }