diff --git a/README.md b/README.md index 764927ca..71b355e8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ public class Application { By default, [Narayana Transactional driver](https://www.narayana.io/docs/api/com/arjuna/ats/jdbc/TransactionalDriver.html) is used to enlist a relational database to a JTA transaction which provides a basic XAResource enlistment and recovery as -well as a simple pooling mechanism. +well as a simple pooling mechanism which is disabled as default. See [TransactionalDriverProperties](narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/TransactionalDriverProperties.java) +for more details. > Be aware that Narayana Transactional driver automatically set transaction isolation level to `Connection.TRANSACTION_SERIALIZABLE`, which might change default behaviour of the used database system! diff --git a/narayana-spring-boot-core/pom.xml b/narayana-spring-boot-core/pom.xml index 77b785cf..bd0bfd44 100644 --- a/narayana-spring-boot-core/pom.xml +++ b/narayana-spring-boot-core/pom.xml @@ -60,6 +60,11 @@ spring-boot-configuration-processor true + + org.springframework + spring-jdbc + true + org.mockito mockito-junit-jupiter diff --git a/narayana-spring-boot-core/src/main/java/com/arjuna/ats/internal/jdbc/ProvidedXADataSourceConnection.java b/narayana-spring-boot-core/src/main/java/com/arjuna/ats/internal/jdbc/ProvidedXADataSourceConnection.java new file mode 100644 index 00000000..07025446 --- /dev/null +++ b/narayana-spring-boot-core/src/main/java/com/arjuna/ats/internal/jdbc/ProvidedXADataSourceConnection.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arjuna.ats.internal.jdbc; + +import java.sql.SQLException; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAResource; + +import jakarta.transaction.Transaction; + +import com.arjuna.ats.internal.jdbc.drivers.modifiers.ConnectionModifier; +import com.arjuna.ats.jdbc.logging.jdbcLogger; +import dev.snowdrop.boot.narayana.core.jdbc.NamedXAResource; + +public class ProvidedXADataSourceConnection implements ConnectionControl, TransactionalDriverXAConnection { + + private final BaseTransactionalDriverXAConnection delegate = new BaseTransactionalDriverXAConnection() { + }; + + public ProvidedXADataSourceConnection(String dbName, String user, String passwd, XADataSource xaDatasource, ConnectionImple conn) { + if (jdbcLogger.logger.isTraceEnabled()) { + jdbcLogger.logger.trace("ProvidedXADataSourceConnection.ProvidedXADataSourceConnection( " + dbName + ", " + user + ", " + passwd + ", " + xaDatasource + " )"); + } + this.delegate._dbName = dbName; + this.delegate._user = user; + this.delegate._passwd = passwd; + this.delegate._theDataSource = xaDatasource; + this.delegate._theArjunaConnection = conn; + } + + @Override + public String dynamicClass() { + return this.delegate.dynamicClass(); + } + + @Override + public String dataSourceName() { + return this.delegate.dataSourceName(); + } + + @Override + public String password() { + return this.delegate.password(); + } + + @Override + public void setModifier(ConnectionModifier cm) { + this.delegate.setModifier(cm); + } + + @Override + public Transaction transaction() { + return this.delegate.transaction(); + } + + @Override + public String url() { + return this.delegate.url(); + } + + @Override + public String user() { + return this.delegate.user(); + } + + @Override + public XADataSource xaDataSource() { + return this.delegate.xaDataSource(); + } + + @Override + public void closeCloseCurrentConnection() throws SQLException { + this.delegate.closeCloseCurrentConnection(); + } + + @Override + public XAConnection getConnection() throws SQLException { + return this.delegate.getConnection(); + } + + @Override + public XAResource getResource() throws SQLException { + if (this.delegate._theXAResource == null) { + this.delegate._theXAResource = new NamedXAResource(this.delegate.getResource(), this.delegate.dataSourceName()); + } + return this.delegate._theXAResource; + } + + @Override + public boolean inuse() { + return this.delegate.inuse(); + } + + @Override + public boolean setTransaction(Transaction tx) { + return this.delegate.setTransaction(tx); + } + + @Override + public boolean validTransaction(Transaction tx) { + return this.delegate.validTransaction(tx); + } +} diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelper.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelper.java index 0330062c..56c8e475 100644 --- a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelper.java +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelper.java @@ -31,14 +31,16 @@ public class DataSourceXAResourceRecoveryHelper implements XAResourceRecoveryHelper, XAResource { private final ConnectionManager connectionManager; + private final String name; /** * Create a new {@link DataSourceXAResourceRecoveryHelper} instance. * * @param xaDataSource the XA data source + * @param name the datasource name or {@code null} */ - public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource) { - this(xaDataSource, null, null); + public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String name) { + this(xaDataSource, null, null, name); } /** @@ -47,9 +49,11 @@ public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource) { * @param xaDataSource the XA data source * @param user the database user or {@code null} * @param password the database password or {@code null} + * @param name the datasource name or {@code null} */ - public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String user, String password) { + public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String user, String password, String name) { this.connectionManager = new ConnectionManager(xaDataSource, user, password); + this.name = name; } @Override @@ -67,7 +71,7 @@ public XAResource[] getXAResources() { } } - return new XAResource[]{this}; + return new XAResource[]{new NamedXAResource(this, this.name)}; } @Override diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapper.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapper.java index 7555b71d..23fca3c2 100644 --- a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapper.java +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapper.java @@ -16,13 +16,23 @@ package dev.snowdrop.boot.narayana.core.jdbc; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + import javax.sql.DataSource; import javax.sql.XADataSource; +import com.arjuna.ats.internal.jdbc.drivers.modifiers.IsSameRMModifier; +import com.arjuna.ats.internal.jdbc.drivers.modifiers.ModifierFactory; +import com.arjuna.ats.internal.jdbc.drivers.modifiers.SupportsMultipleConnectionsModifier; import com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule; import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper; import dev.snowdrop.boot.narayana.core.properties.RecoveryCredentialsProperties; +import dev.snowdrop.boot.narayana.core.properties.TransactionalDriverProperties; +import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.jdbc.support.JdbcUtils; /** * An {@link XADataSourceWrapper} implementation which handles {@link XAResourceRecoveryHelper} creation and @@ -33,12 +43,13 @@ public class GenericXADataSourceWrapper implements XADataSourceWrapper { private final XARecoveryModule xaRecoveryModule; + private final TransactionalDriverProperties transactionalDriverProperties; private final RecoveryCredentialsProperties recoveryCredentials; /** * Create a new {@link GenericXADataSourceWrapper} instance. * - * @param xaRecoveryModule recovery module to register data source with. + * @param xaRecoveryModule recovery module to register data source with. */ public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule) { this(xaRecoveryModule, RecoveryCredentialsProperties.DEFAULT); @@ -47,11 +58,34 @@ public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule) { /** * Create a new {@link GenericXADataSourceWrapper} instance. * - * @param xaRecoveryModule recovery module to register data source with. - * @param recoveryCredentials credentials for recovery helper + * @param xaRecoveryModule recovery module to register data source with. + * @param recoveryCredentials credentials for recovery helper */ public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule, RecoveryCredentialsProperties recoveryCredentials) { + this(xaRecoveryModule, new TransactionalDriverProperties(), recoveryCredentials); + } + + /** + * Create a new {@link GenericXADataSourceWrapper} instance. + * + * @param xaRecoveryModule recovery module to register data source with. + * @param transactionalDriverProperties Transactional driver properties + */ + public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule, TransactionalDriverProperties transactionalDriverProperties) { + this(xaRecoveryModule, transactionalDriverProperties, RecoveryCredentialsProperties.DEFAULT); + } + + /** + * Create a new {@link GenericXADataSourceWrapper} instance. + * + * @param xaRecoveryModule recovery module to register data source with. + * @param transactionalDriverProperties Transactional driver properties + * @param recoveryCredentials credentials for recovery helper + */ + public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule, TransactionalDriverProperties transactionalDriverProperties, + RecoveryCredentialsProperties recoveryCredentials) { this.xaRecoveryModule = xaRecoveryModule; + this.transactionalDriverProperties = transactionalDriverProperties; this.recoveryCredentials = recoveryCredentials; } @@ -66,14 +100,34 @@ public GenericXADataSourceWrapper(XARecoveryModule xaRecoveryModule, RecoveryCre public DataSource wrapDataSource(XADataSource dataSource) throws Exception { XAResourceRecoveryHelper recoveryHelper = getRecoveryHelper(dataSource); this.xaRecoveryModule.addXAResourceRecoveryHelper(recoveryHelper); - return new NarayanaDataSource(dataSource); + registerModifier(dataSource); + return new NarayanaDataSource(dataSource, this.transactionalDriverProperties); } private XAResourceRecoveryHelper getRecoveryHelper(XADataSource dataSource) { if (this.recoveryCredentials.isValid()) { return new DataSourceXAResourceRecoveryHelper(dataSource, this.recoveryCredentials.getUser(), - this.recoveryCredentials.getPassword()); + this.recoveryCredentials.getPassword(), this.transactionalDriverProperties.getName()); + } + return new DataSourceXAResourceRecoveryHelper(dataSource, this.transactionalDriverProperties.getName()); + } + + private void registerModifier(XADataSource dataSource) throws SQLException { + try (Connection conn = dataSource.getXAConnection().getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + String driver = metaData.getDriverName(); + int major = metaData.getDriverMajorVersion(); + int minor = metaData.getDriverMinorVersion(); + switch (this.transactionalDriverProperties.getModifier()) { + case DEFAULT -> { + switch (DatabaseDriver.fromProductName(JdbcUtils.commonDatabaseName(metaData.getDatabaseProductName()))) { + case DB2, H2, MYSQL, ORACLE, SQLSERVER -> ModifierFactory.putModifier(driver, major, minor, IsSameRMModifier.class.getName()); + case POSTGRESQL -> ModifierFactory.putModifier(driver, major, minor, SupportsMultipleConnectionsModifier.class.getName()); + } + } + case IS_SAME_RM -> ModifierFactory.putModifier(driver, major, minor, IsSameRMModifier.class.getName()); + case SUPPORTS_MULTIPLE_CONNECTIONS -> ModifierFactory.putModifier(driver, major, minor, SupportsMultipleConnectionsModifier.class.getName()); + } } - return new DataSourceXAResourceRecoveryHelper(dataSource); } } diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NamedXAResource.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NamedXAResource.java new file mode 100644 index 00000000..210bce87 --- /dev/null +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NamedXAResource.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.snowdrop.boot.narayana.core.jdbc; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.jboss.tm.XAResourceWrapper; + +public class NamedXAResource implements XAResourceWrapper { + + private static final String PRODUCT_NAME = NamedXAResource.class.getPackage().getImplementationTitle(); + private static final String PRODUCT_VERSION = NamedXAResource.class.getPackage().getImplementationVersion(); + + private final XAResource xaResource; + private final String name; + + public NamedXAResource(XAResource xaResource, String name) { + this.xaResource = xaResource; + this.name = name; + } + + @Override + public XAResource getResource() { + return this.xaResource; + } + + @Override + public String getProductName() { + return PRODUCT_NAME; + } + + @Override + public String getProductVersion() { + return PRODUCT_VERSION; + } + + @Override + public String getJndiName() { + return this.name; + } + + @Override + public void commit(Xid xid, boolean onePhase) throws XAException { + getResource().commit(xid, onePhase); + } + + @Override + public void end(Xid xid, int flags) throws XAException { + getResource().end(xid, flags); + } + + @Override + public void forget(Xid xid) throws XAException { + getResource().forget(xid); + } + + @Override + public int getTransactionTimeout() throws XAException { + return getResource().getTransactionTimeout(); + } + + @Override + public boolean isSameRM(XAResource xaRes) throws XAException { + if (xaRes instanceof NamedXAResource namedXaResource) { + return getResource().isSameRM((namedXaResource).getResource()); + } + return false; + } + + @Override + public int prepare(Xid xid) throws XAException { + return getResource().prepare(xid); + } + + @Override + public Xid[] recover(int flag) throws XAException { + return getResource().recover(flag); + } + + @Override + public void rollback(Xid xid) throws XAException { + getResource().rollback(xid); + } + + @Override + public boolean setTransactionTimeout(int seconds) throws XAException { + return getResource().setTransactionTimeout(seconds); + } + + @Override + public void start(Xid xid, int flags) throws XAException { + getResource().start(xid, flags); + } +} diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSource.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSource.java index 173b88ec..fbf1a605 100644 --- a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSource.java +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSource.java @@ -28,6 +28,7 @@ import com.arjuna.ats.internal.jdbc.ConnectionManager; import com.arjuna.ats.jdbc.TransactionalDriver; +import dev.snowdrop.boot.narayana.core.properties.TransactionalDriverProperties; /** * {@link DataSource} implementation wrapping {@link XADataSource} and using @@ -38,30 +39,38 @@ public class NarayanaDataSource implements DataSource { private final XADataSource xaDataSource; + private final TransactionalDriverProperties transactionalDriverProperties; /** * Create a new {@link NarayanaDataSource} instance. * - * @param xaDataSource the XA DataSource + * @param xaDataSource the XA DataSource + * @param transactionalDriverProperties Transactional driver pool properties */ - public NarayanaDataSource(XADataSource xaDataSource) { + public NarayanaDataSource(XADataSource xaDataSource, TransactionalDriverProperties transactionalDriverProperties) { this.xaDataSource = xaDataSource; + this.transactionalDriverProperties = transactionalDriverProperties; } - @Override - public Connection getConnection() throws SQLException { + private Properties createProperties() { Properties properties = new Properties(); properties.put(TransactionalDriver.XADataSource, this.xaDataSource); - return ConnectionManager.create(null, properties); + properties.put(TransactionalDriver.poolConnections, String.valueOf(this.transactionalDriverProperties.getPool().isEnabled())); + properties.put(TransactionalDriver.maxConnections, this.transactionalDriverProperties.getPool().getMaxConnections()); + return properties; + } + + @Override + public Connection getConnection() throws SQLException { + return ConnectionManager.create(this.transactionalDriverProperties.getName(), createProperties()); } @Override public Connection getConnection(String username, String password) throws SQLException { - Properties properties = new Properties(); - properties.put(TransactionalDriver.XADataSource, this.xaDataSource); + Properties properties = createProperties(); properties.put(TransactionalDriver.userName, username); properties.put(TransactionalDriver.password, password); - return ConnectionManager.create(null, properties); + return ConnectionManager.create(this.transactionalDriverProperties.getName(), properties); } @Override diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaProperties.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaProperties.java index 0048a539..68d642a6 100644 --- a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaProperties.java +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaProperties.java @@ -123,6 +123,12 @@ public class NarayanaProperties { private List expiryScanners = List.of( "com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner"); + /** + * Narayana Transactional driver specific properties. + */ + @NestedConfigurationProperty + private final TransactionalDriverProperties transactionalDriver = new TransactionalDriverProperties(); + /** * MessagingHub specific properties used if pooled connection factory wrapper is enabled. * See ... for the list of supported properties. @@ -273,6 +279,10 @@ public void setRecoveryJmsCredentials(RecoveryCredentialsProperties recoveryJmsC this.recoveryJmsCredentials = recoveryJmsCredentials; } + public TransactionalDriverProperties getTransactionalDriver() { + return this.transactionalDriver; + } + public MessagingHubConnectionFactoryProperties getMessaginghub() { return this.messaginghub; } diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializer.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializer.java index 29020731..9ae42b91 100644 --- a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializer.java +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializer.java @@ -28,6 +28,7 @@ import com.arjuna.ats.arjuna.common.CoreEnvironmentBeanException; import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean; import com.arjuna.ats.arjuna.common.RecoveryEnvironmentBean; +import com.arjuna.ats.jdbc.common.JDBCEnvironmentBean; import com.arjuna.ats.jta.common.JTAEnvironmentBean; import com.arjuna.common.internal.util.propertyservice.BeanPopulator; import org.springframework.beans.factory.InitializingBean; @@ -63,6 +64,8 @@ public void afterPropertiesSet() { setCommitMarkableResourceJNDINames(this.properties.getCommitMarkableResourceJNDINames()); setRecoveryModules(this.properties.getRecoveryModules()); setExpiryScanners(this.properties.getExpiryScanners()); + setDefaultIsolationLevel(this.properties.getTransactionalDriver().getDefaultIsolationLevel().getLevel()); + setDefaultIsSameRMOverride(this.properties.getTransactionalDriver().isDefaultIsSameRMOverride()); } private void setNodeIdentifier(String nodeIdentifier, boolean shortenNodeIdentifierIfNecessary) { @@ -147,6 +150,14 @@ private void setExpiryScanners(List expiryScanners) { getPopulator(RecoveryEnvironmentBean.class).setExpiryScannerClassNames(expiryScanners); } + private void setDefaultIsolationLevel(int defaultIsolationLevel) { + getPopulator(JDBCEnvironmentBean.class).setIsolationLevel(defaultIsolationLevel); + } + + private void setDefaultIsSameRMOverride(boolean defaultIsSameRMOverride) { + getPopulator(JDBCEnvironmentBean.class).setDefaultIsSameRMOverride(defaultIsSameRMOverride); + } + private T getPopulator(Class beanClass) { return BeanPopulator.getDefaultInstance(beanClass); } diff --git a/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/TransactionalDriverProperties.java b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/TransactionalDriverProperties.java new file mode 100644 index 00000000..87ec5e51 --- /dev/null +++ b/narayana-spring-boot-core/src/main/java/dev/snowdrop/boot/narayana/core/properties/TransactionalDriverProperties.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.snowdrop.boot.narayana.core.properties; + +import java.sql.Connection; + +public class TransactionalDriverProperties { + + private String name = "jdbc"; + private Modifier modifier = Modifier.DEFAULT; + private IsolationLevel defaultIsolationLevel = IsolationLevel.TRANSACTION_SERIALIZABLE; + private boolean defaultIsSameRMOverride = false; + private Pool pool = new Pool(); + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Modifier getModifier() { + return this.modifier; + } + + public void setModifier(Modifier modifier) { + this.modifier = modifier; + } + + public IsolationLevel getDefaultIsolationLevel() { + return this.defaultIsolationLevel; + } + + public void setDefaultIsolationLevel(IsolationLevel defaultIsolationLevel) { + this.defaultIsolationLevel = defaultIsolationLevel; + } + + public boolean isDefaultIsSameRMOverride() { + return this.defaultIsSameRMOverride; + } + + public void setDefaultIsSameRMOverride(boolean defaultIsSameRMOverride) { + this.defaultIsSameRMOverride = defaultIsSameRMOverride; + } + + public Pool getPool() { + return this.pool; + } + + public void setPool(Pool pool) { + this.pool = pool; + } + + public enum Modifier { + /** + * Register {@link com.arjuna.ats.internal.jdbc.drivers.modifiers.IsSameRMModifier} for used JDBC driver. + */ + IS_SAME_RM, + /** + * Register {@link com.arjuna.ats.internal.jdbc.drivers.modifiers.SupportsMultipleConnectionsModifier} for used JDBC driver. + */ + SUPPORTS_MULTIPLE_CONNECTIONS, + /** + * Use default modifier. + */ + DEFAULT; + } + + public enum IsolationLevel { + + /** + * Transaction isolation level TRANSACTION_READ_UNCOMMITTED. + * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED + */ + TRANSACTION_READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED), + /** + * Transaction isolation level TRANSACTION_READ_COMMITTED. + * @see java.sql.Connection#TRANSACTION_READ_COMMITTED + */ + TRANSACTION_READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED), + /** + * Transaction isolation level TRANSACTION_REPEATABLE_READ. + * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ + */ + TRANSACTION_REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), + /** + * Transaction isolation level TRANSACTION_SERIALIZABLE. + * @see java.sql.Connection#TRANSACTION_SERIALIZABLE + */ + TRANSACTION_SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE); + + private final int level; + + IsolationLevel(int level) { + this.level = level; + } + + public int getLevel() { + return this.level; + } + } + + public static class Pool { + + private boolean enabled = false; + private int maxConnections = 10; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getMaxConnections() { + return this.maxConnections; + } + + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + } +} diff --git a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelperTests.java b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelperTests.java index 1fa410ad..e52c793a 100644 --- a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelperTests.java +++ b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/DataSourceXAResourceRecoveryHelperTests.java @@ -56,7 +56,7 @@ class DataSourceXAResourceRecoveryHelperTests { @BeforeEach void before() throws SQLException { - this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.mockXaDataSource); + this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.mockXaDataSource, null); given(this.mockXaDataSource.getXAConnection()).willReturn(this.mockXaConnection); given(this.mockXaConnection.getXAResource()).willReturn(this.mockXaResource); @@ -66,17 +66,17 @@ void before() throws SQLException { void shouldCreateConnectionAndGetXAResource() throws SQLException { XAResource[] xaResources = this.recoveryHelper.getXAResources(); assertThat(xaResources).hasSize(1); - assertThat(xaResources[0]).isSameAs(this.recoveryHelper); + assertThat(((NamedXAResource) xaResources[0]).getResource()).isSameAs(this.recoveryHelper); verify(this.mockXaDataSource).getXAConnection(); } @Test void shouldCreateConnectionWithCredentialsAndGetXAResource() throws SQLException { given(this.mockXaDataSource.getXAConnection(anyString(), anyString())).willReturn(this.mockXaConnection); - this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.mockXaDataSource, "username", "password"); + this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.mockXaDataSource, "username", "password", null); XAResource[] xaResources = this.recoveryHelper.getXAResources(); assertThat(xaResources).hasSize(1); - assertThat(xaResources[0]).isSameAs(this.recoveryHelper); + assertThat(((NamedXAResource) xaResources[0]).getResource()).isSameAs(this.recoveryHelper); verify(this.mockXaDataSource).getXAConnection("username", "password"); } diff --git a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapperTests.java b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapperTests.java index e88a7d1f..8cb716b2 100644 --- a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapperTests.java +++ b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/GenericXADataSourceWrapperTests.java @@ -16,7 +16,12 @@ package dev.snowdrop.boot.narayana.core.jdbc; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + import javax.sql.DataSource; +import javax.sql.XAConnection; import javax.sql.XADataSource; import com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule; @@ -43,6 +48,15 @@ class GenericXADataSourceWrapperTests { @Mock private XADataSource mockXaDataSource; + @Mock + private XAConnection mockXaConnection; + + @Mock + private Connection mockConnection; + + @Mock + private DatabaseMetaData mockDatabaseMetaData; + @Mock private XARecoveryModule mockXaRecoveryModule; @@ -52,7 +66,11 @@ class GenericXADataSourceWrapperTests { private GenericXADataSourceWrapper wrapper; @BeforeEach - void before() { + void before() throws SQLException { + given(this.mockXaDataSource.getXAConnection()).willReturn(this.mockXaConnection); + given(this.mockXaConnection.getConnection()).willReturn(this.mockConnection); + given(this.mockConnection.getMetaData()).willReturn(this.mockDatabaseMetaData); + given(this.mockDatabaseMetaData.getDatabaseProductName()).willReturn(""); this.wrapper = new GenericXADataSourceWrapper(this.mockXaRecoveryModule, this.mockRecoveryCredentialsProperties); } diff --git a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSourceTests.java b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSourceTests.java index 65c3ba4f..b65bec96 100644 --- a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSourceTests.java +++ b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/jdbc/NarayanaDataSourceTests.java @@ -27,6 +27,7 @@ import javax.sql.XADataSource; import com.arjuna.ats.internal.jdbc.ConnectionImple; +import dev.snowdrop.boot.narayana.core.properties.TransactionalDriverProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -50,11 +51,13 @@ class NarayanaDataSourceTests { @Mock private XADataSource mockXaDataSource; + private TransactionalDriverProperties transactionalDriverProperties; private NarayanaDataSource dataSourceBean; @BeforeEach void before() { - this.dataSourceBean = new NarayanaDataSource(this.mockXaDataSource); + this.transactionalDriverProperties = new TransactionalDriverProperties(); + this.dataSourceBean = new NarayanaDataSource(this.mockXaDataSource, this.transactionalDriverProperties); } @Test @@ -79,6 +82,24 @@ void shouldUnwrapXaDataSource() throws SQLException { assertThat(this.dataSourceBean.unwrap(XADataSource.class)).isSameAs(this.mockXaDataSource); } + @Test + void shouldGetSameConnection() throws SQLException { + this.transactionalDriverProperties.getPool().setEnabled(true); + Connection connection1 = this.dataSourceBean.getConnection(); + connection1.close(); + Connection connection2 = this.dataSourceBean.getConnection(); + assertThat(connection2).isSameAs(connection1); + } + + @Test + void shouldNotGetSameConnection() throws SQLException { + this.transactionalDriverProperties.getPool().setEnabled(false); + Connection connection1 = this.dataSourceBean.getConnection(); + connection1.close(); + Connection connection2 = this.dataSourceBean.getConnection(); + assertThat(connection2).isNotSameAs(connection1); + } + @Test void shouldGetConnectionAndCommit() throws SQLException { DatabaseMetaData mockMetaData = mock(DatabaseMetaData.class); diff --git a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializerTests.java b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializerTests.java index bc209897..abb42e01 100644 --- a/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializerTests.java +++ b/narayana-spring-boot-core/src/test/java/dev/snowdrop/boot/narayana/core/properties/NarayanaPropertiesInitializerTests.java @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; +import java.sql.Connection; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ import com.arjuna.ats.arjuna.common.CoreEnvironmentBean; import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean; import com.arjuna.ats.arjuna.common.RecoveryEnvironmentBean; +import com.arjuna.ats.jdbc.common.JDBCEnvironmentBean; import com.arjuna.ats.jta.common.JTAEnvironmentBean; import com.arjuna.common.internal.util.propertyservice.BeanPopulator; import org.junit.jupiter.api.AfterEach; @@ -112,6 +114,12 @@ void shouldSetDefaultProperties() { assertThat(BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class) .getXaResourceRecoveryClassNames()).isEmpty(); + + assertThat(BeanPopulator.getDefaultInstance(JDBCEnvironmentBean.class) + .getIsolationLevel()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + + assertThat(BeanPopulator.getDefaultInstance(JDBCEnvironmentBean.class) + .getDefaultIsSameRMOverride()).isFalse(); } @Test diff --git a/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledRecoveryIT.java b/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledRecoveryIT.java new file mode 100644 index 00000000..e614f2ad --- /dev/null +++ b/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledRecoveryIT.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.snowdrop.boot.narayana.pooled; + +import dev.snowdrop.boot.narayana.app.TestApplication; +import dev.snowdrop.boot.narayana.generic.GenericRecoveryIT; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = TestApplication.class, properties = {"narayana.transactionalDriver.pool.enabled=true"}) +public class SimplePooledRecoveryIT extends GenericRecoveryIT { +} diff --git a/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledTransactionalIT.java b/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledTransactionalIT.java new file mode 100644 index 00000000..b5663baa --- /dev/null +++ b/narayana-spring-boot-starter-it/src/test/java/dev/snowdrop/boot/narayana/pooled/SimplePooledTransactionalIT.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Red Hat, Inc, and individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.snowdrop.boot.narayana.pooled; + +import dev.snowdrop.boot.narayana.app.TestApplication; +import dev.snowdrop.boot.narayana.generic.GenericTransactionalIT; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = TestApplication.class, properties = {"narayana.transactionalDriver.pool.enabled=true"}) +public class SimplePooledTransactionalIT extends GenericTransactionalIT { +} diff --git a/narayana-spring-boot-starter/src/main/java/dev/snowdrop/boot/narayana/autoconfigure/NarayanaAutoConfiguration.java b/narayana-spring-boot-starter/src/main/java/dev/snowdrop/boot/narayana/autoconfigure/NarayanaAutoConfiguration.java index 5298727b..ea0ff96c 100644 --- a/narayana-spring-boot-starter/src/main/java/dev/snowdrop/boot/narayana/autoconfigure/NarayanaAutoConfiguration.java +++ b/narayana-spring-boot-starter/src/main/java/dev/snowdrop/boot/narayana/autoconfigure/NarayanaAutoConfiguration.java @@ -164,6 +164,7 @@ static class GenericJdbcConfiguration { public XADataSourceWrapper xaDataSourceWrapper(NarayanaProperties narayanaProperties, XARecoveryModule xaRecoveryModule) { return new GenericXADataSourceWrapper(xaRecoveryModule, + narayanaProperties.getTransactionalDriver(), narayanaProperties.getRecoveryDbCredentials()); }