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 <stari...@gmail.com> Authored: Tue Mar 27 13:06:22 2018 +0300 Committer: Nikita Timofeev <stari...@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; + } }