From ee80adef82a589e2389c78276d2ac600fde067e9 Mon Sep 17 00:00:00 2001 From: Gytis Trikleris Date: Tue, 4 Jul 2017 19:16:16 +0200 Subject: [PATCH] Improve Narayana JDBC helper to create connections when needed for recovery --- .../boot/jta/narayana/ConnectionManager.java | 172 +++++++++++++++ .../DataSourceXAResourceRecoveryHelper.java | 124 +++-------- .../narayana/NarayanaXADataSourceWrapper.java | 13 +- .../boot/jta/narayana/XAResourceConsumer.java | 32 +++ .../boot/jta/narayana/XAResourceFunction.java | 33 +++ .../jta/narayana/ConnectionManagerTests.java | 204 ++++++++++++++++++ ...taSourceXAResourceRecoveryHelperTests.java | 115 +++++----- .../NarayanaXADataSourceWrapperTests.java | 2 +- 8 files changed, 526 insertions(+), 169 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/ConnectionManager.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceConsumer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceFunction.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/ConnectionManagerTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/ConnectionManager.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/ConnectionManager.java new file mode 100644 index 000000000000..510b9b2f0815 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/ConnectionManager.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * 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 org.springframework.boot.jta.narayana; + +import java.sql.SQLException; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Manager able to connect and disconnect when needed for the required operations. If SQL connection is already available, it + * will simply executed requested operation. If SQL connection is not available, it will create it, execute a requested + * operation, and close the connection. + * + * @author Gytis Trikleris + */ +class ConnectionManager { + + private static final Log logger = LogFactory.getLog(DataSourceXAResourceRecoveryHelper.class); + + private final XADataSource dataSource; + + private final String user; + + private final String password; + + private XAConnection connection; + + private XAResource resource; + + /** + * Create a new {@link ConnectionManager} instance. + * @param dataSource DataSource to be used when handling connections. + * @param user Username with which connection should be created. + * @param password Password of the user. + */ + ConnectionManager(XADataSource dataSource, String user, String password) { + this.dataSource = dataSource; + this.user = user; + this.password = password; + } + + /** + * Invoke {@link XAResourceConsumer} accept method before making sure that SQL connection is available. Current connection + * is used if one is available. If connection is not available, new connection is created before the accept call and closed + * after it. + * + * @param consumer {@link XAResourceConsumer} to be executed. + * @throws XAException if connection cannot be created or exception thrown by the consumer. + */ + void connectAndAccept(XAResourceConsumer consumer) throws XAException { + if (isConnected()) { + consumer.accept(this.resource); + return; + } + + connect(); + try { + consumer.accept(this.resource); + } + finally { + disconnect(); + } + } + + /** + * Invoke {@link XAResourceFunction} apply method before making sure that SQL connection is available. Current connection is + * used if one is available. If connection is not available, new connection is created before the apply call and closed + * after it. + * + * @param function {@link XAResourceFunction} to be executed. + * @param Return type of the {@link XAResourceFunction}. + * @return The result of {@link XAResourceFunction}. + * @throws XAException if connection cannot be created or exception thrown by the function. + */ + T connectAndApply(XAResourceFunction function) throws XAException { + if (isConnected()) { + return function.apply(this.resource); + } + + connect(); + try { + return function.apply(this.resource); + } + finally { + disconnect(); + } + } + + /** + * Create SQL connection if one is not available. + * + * @throws XAException if connection cannot be created. + */ + public void connect() throws XAException { + if (isConnected()) { + return; + } + + try { + this.connection = getXaConnection(); + this.resource = this.connection.getXAResource(); + } + catch (SQLException e) { + if (this.connection != null) { + try { + this.connection.close(); + } + catch (SQLException ignore) { + } + } + logger.warn("Failed to create connection", e); + throw new XAException(XAException.XAER_RMFAIL); + } + } + + /** + * Close current SQL connection. + */ + public void disconnect() { + if (!isConnected()) { + return; + } + + try { + this.connection.close(); + } + catch (SQLException e) { + logger.warn("Failed to close connection", e); + } + finally { + this.connection = null; + this.resource = null; + } + } + + /** + * Check if SQL connection is active. + * + * @return {@code true} if SQL connection is active. + */ + public boolean isConnected() { + return this.connection != null && this.resource != null; + } + + private XAConnection getXaConnection() throws SQLException { + if (this.user == null && this.password == null) { + return this.dataSource.getXAConnection(); + } + return this.dataSource.getXAConnection(this.user, this.password); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java index 5f33ee29806f..05394b6db05d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java +++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java @@ -16,17 +16,11 @@ package org.springframework.boot.jta.narayana; -import java.sql.SQLException; - -import javax.sql.XAConnection; -import javax.sql.XADataSource; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; @@ -42,39 +36,15 @@ public class DataSourceXAResourceRecoveryHelper private static final XAResource[] NO_XA_RESOURCES = {}; - private static final Log logger = LogFactory - .getLog(DataSourceXAResourceRecoveryHelper.class); - - private final XADataSource xaDataSource; - - private final String user; - - private final String password; - - private XAConnection xaConnection; - - private XAResource delegate; + private final ConnectionManager connectionManager; /** * Create a new {@link DataSourceXAResourceRecoveryHelper} instance. - * @param xaDataSource the XA data source + * @param connectionManager SQL connection manager. */ - public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource) { - this(xaDataSource, null, null); - } - - /** - * Create a new {@link DataSourceXAResourceRecoveryHelper} instance. - * @param xaDataSource the XA data source - * @param user the database user or {@code null} - * @param password the database password or {@code null} - */ - public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String user, - String password) { - Assert.notNull(xaDataSource, "XADataSource must not be null"); - this.xaDataSource = xaDataSource; - this.user = user; - this.password = password; + public DataSourceXAResourceRecoveryHelper(ConnectionManager connectionManager) { + Assert.notNull(connectionManager, "ConnectionManager must not be null"); + this.connectionManager = connectionManager; } @Override @@ -84,107 +54,73 @@ public boolean initialise(String properties) { @Override public XAResource[] getXAResources() { - if (connect()) { - return new XAResource[] { this }; - } - return NO_XA_RESOURCES; - } - - private boolean connect() { - if (this.delegate == null) { + if (!this.connectionManager.isConnected()) { try { - this.xaConnection = getXaConnection(); - this.delegate = this.xaConnection.getXAResource(); + this.connectionManager.connect(); } - catch (SQLException ex) { - logger.warn("Failed to create connection", ex); - return false; + catch (XAException ignored) { + return NO_XA_RESOURCES; } } - return true; - } - private XAConnection getXaConnection() throws SQLException { - if (this.user == null && this.password == null) { - return this.xaDataSource.getXAConnection(); - } - return this.xaDataSource.getXAConnection(this.user, this.password); + return new XAResource[] { this }; } @Override - public Xid[] recover(int flag) throws XAException { + public Xid[] recover(final int flag) throws XAException { try { - return getDelegate(true).recover(flag); + return this.connectionManager.connectAndApply(delegate -> delegate.recover(flag)); } finally { if (flag == XAResource.TMENDRSCAN) { - disconnect(); + this.connectionManager.disconnect(); } } } - private void disconnect() throws XAException { - try { - this.xaConnection.close(); - } - catch (SQLException e) { - logger.warn("Failed to close connection", e); - } - finally { - this.xaConnection = null; - this.delegate = null; - } - } - @Override - public void start(Xid xid, int flags) throws XAException { - getDelegate(true).start(xid, flags); + public void start(final Xid xid, final int flags) throws XAException { + this.connectionManager.connectAndAccept(delegate -> delegate.start(xid, flags)); } @Override - public void end(Xid xid, int flags) throws XAException { - getDelegate(true).end(xid, flags); + public void end(final Xid xid, final int flags) throws XAException { + this.connectionManager.connectAndAccept(delegate -> delegate.end(xid, flags)); } @Override - public int prepare(Xid xid) throws XAException { - return getDelegate(true).prepare(xid); + public int prepare(final Xid xid) throws XAException { + return this.connectionManager.connectAndApply(delegate -> delegate.prepare(xid)); } @Override - public void commit(Xid xid, boolean onePhase) throws XAException { - getDelegate(true).commit(xid, onePhase); + public void commit(final Xid xid, final boolean onePhase) throws XAException { + this.connectionManager.connectAndAccept(delegate -> delegate.commit(xid, onePhase)); } @Override - public void rollback(Xid xid) throws XAException { - getDelegate(true).rollback(xid); + public void rollback(final Xid xid) throws XAException { + this.connectionManager.connectAndAccept(delegate -> delegate.rollback(xid)); } @Override - public boolean isSameRM(XAResource xaResource) throws XAException { - return getDelegate(true).isSameRM(xaResource); + public boolean isSameRM(final XAResource xaResource) throws XAException { + return this.connectionManager.connectAndApply(delegate -> delegate.isSameRM(xaResource)); } @Override - public void forget(Xid xid) throws XAException { - getDelegate(true).forget(xid); + public void forget(final Xid xid) throws XAException { + this.connectionManager.connectAndAccept(delegate -> delegate.forget(xid)); } @Override public int getTransactionTimeout() throws XAException { - return getDelegate(true).getTransactionTimeout(); + return this.connectionManager.connectAndApply(XAResource::getTransactionTimeout); } @Override - public boolean setTransactionTimeout(int seconds) throws XAException { - return getDelegate(true).setTransactionTimeout(seconds); - } - - private XAResource getDelegate(boolean required) { - Assert.state(this.delegate != null || !required, - "Connection has not been opened"); - return this.delegate; + public boolean setTransactionTimeout(final int seconds) throws XAException { + return this.connectionManager.connectAndApply(delegate -> delegate.setTransactionTimeout(seconds)); } } diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java index e7705be90a76..02b63ca3ccb4 100644 --- a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java +++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java @@ -52,18 +52,11 @@ public NarayanaXADataSourceWrapper(NarayanaRecoveryManagerBean recoveryManager, @Override public DataSource wrapDataSource(XADataSource dataSource) { - XAResourceRecoveryHelper recoveryHelper = getRecoveryHelper(dataSource); + ConnectionManager connectionManager = new ConnectionManager(dataSource, this.properties.getRecoveryDbUser(), + this.properties.getRecoveryDbPass()); + XAResourceRecoveryHelper recoveryHelper = new DataSourceXAResourceRecoveryHelper(connectionManager); this.recoveryManager.registerXAResourceRecoveryHelper(recoveryHelper); return new NarayanaDataSourceBean(dataSource); } - private XAResourceRecoveryHelper getRecoveryHelper(XADataSource dataSource) { - if (this.properties.getRecoveryDbUser() == null - && this.properties.getRecoveryDbPass() == null) { - return new DataSourceXAResourceRecoveryHelper(dataSource); - } - return new DataSourceXAResourceRecoveryHelper(dataSource, - this.properties.getRecoveryDbUser(), this.properties.getRecoveryDbPass()); - } - } diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceConsumer.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceConsumer.java new file mode 100644 index 000000000000..b39d857cfd05 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceConsumer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * 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 org.springframework.boot.jta.narayana; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +/** + * Functional consumer interface which can throw {@link XAException}. + * + * @author Gytis Trikleris + */ +@FunctionalInterface +public interface XAResourceConsumer { + + void accept(XAResource xaResource) throws XAException; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceFunction.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceFunction.java new file mode 100644 index 000000000000..a045ed38aba0 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/XAResourceFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * 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 org.springframework.boot.jta.narayana; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +/** + * Functional function interface which can throw {@link XAException}. + * + * @author Gytis Trikleris + * @param Function response type + */ +@FunctionalInterface +public interface XAResourceFunction { + + T apply(XAResource xaResource) throws XAException; + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/ConnectionManagerTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/ConnectionManagerTests.java new file mode 100644 index 000000000000..5d4cca5b985d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/ConnectionManagerTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * 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 org.springframework.boot.jta.narayana; + +import java.sql.SQLException; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ConnectionManager}. + * + * @author Gytis Trikleris + */ +public class ConnectionManagerTests { + + @Mock + private XADataSource xaDataSource; + + @Mock + private XAConnection xaConnection; + + @Mock + private XAResource xaResource; + + private String user = "testUser"; + + private String pass = "testPass"; + + private ConnectionManager connectionManager; + + @Before + public void before() throws SQLException { + MockitoAnnotations.initMocks(this); + + given(this.xaDataSource.getXAConnection()).willReturn(this.xaConnection); + given(this.xaDataSource.getXAConnection(anyString(), anyString())).willReturn(this.xaConnection); + given(this.xaConnection.getXAResource()).willReturn(this.xaResource); + + this.connectionManager = new ConnectionManager(this.xaDataSource, this.user, this.pass); + } + + @Test + public void shouldConnectWithoutCredentials() throws XAException, SQLException { + this.connectionManager = new ConnectionManager(this.xaDataSource, null, null); + this.connectionManager.connect(); + verify(this.xaDataSource, times(1)).getXAConnection(); + verify(this.xaDataSource, times(0)).getXAConnection(anyString(), anyString()); + assertThat(this.connectionManager.isConnected()).isTrue(); + } + + @Test + public void shouldConnectWithCredentials() throws XAException, SQLException { + this.connectionManager.connect(); + verify(this.xaDataSource, times(0)).getXAConnection(); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + assertThat(this.connectionManager.isConnected()).isTrue(); + } + + @Test + public void shouldNotConnectWithExistingConnection() throws XAException, SQLException { + this.connectionManager.connect(); + this.connectionManager.connect(); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + assertThat(this.connectionManager.isConnected()).isTrue(); + } + + @Test + public void shouldFailToConnect() throws XAException, SQLException { + given(this.xaDataSource.getXAConnection(anyString(), anyString())).willThrow(new SQLException("test")); + try { + this.connectionManager.connect(); + fail("XAException was expected"); + } + catch (XAException e) { + assertThat(e.errorCode).isEqualTo(XAException.XAER_RMFAIL); + } + + } + + @Test + public void shouldDisconnect() throws XAException, SQLException { + this.connectionManager.connect(); + this.connectionManager.disconnect(); + verify(this.xaConnection, times(1)).close(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldFailToDisconnect() throws XAException, SQLException { + willThrow(new SQLException("test")).given(this.xaConnection).close(); + this.connectionManager.connect(); + this.connectionManager.disconnect(); + verify(this.xaConnection, times(1)).close(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldNotDisconnectWithoutConnection() throws XAException, SQLException { + this.connectionManager.disconnect(); + verify(this.xaConnection, times(0)).close(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldAcceptWithoutConnecting() throws XAException, SQLException { + this.connectionManager.connect(); + this.connectionManager.connectAndAccept(XAResource::getTransactionTimeout); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaResource, times(1)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isTrue(); + } + + @Test + public void shouldConnectAndAccept() throws XAException, SQLException { + this.connectionManager.connectAndAccept(XAResource::getTransactionTimeout); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaConnection, times(1)).close(); + verify(this.xaResource, times(1)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldFailToConnectAndNotAccept() throws XAException, SQLException { + given(this.xaDataSource.getXAConnection(anyString(), anyString())).willThrow(new SQLException("test")); + try { + this.connectionManager.connectAndAccept(XAResource::getTransactionTimeout); + fail("Exception expected"); + } + catch (XAException ignored) { + } + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaConnection, times(0)).close(); + verify(this.xaConnection, times(0)).getXAResource(); + verify(this.xaResource, times(0)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldApplyWithoutConnecting() throws XAException, SQLException { + this.connectionManager.connect(); + this.connectionManager.connectAndApply(XAResource::getTransactionTimeout); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaConnection, times(1)).getXAResource(); + verify(this.xaResource, times(1)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isTrue(); + } + + @Test + public void shouldConnectAndApply() throws XAException, SQLException { + this.connectionManager.connectAndApply(XAResource::getTransactionTimeout); + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaConnection, times(1)).close(); + verify(this.xaConnection, times(1)).getXAResource(); + verify(this.xaResource, times(1)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + + @Test + public void shouldFailToConnectAndNotApply() throws XAException, SQLException { + given(this.xaDataSource.getXAConnection(anyString(), anyString())).willThrow(new SQLException("test")); + try { + this.connectionManager.connectAndApply(XAResource::getTransactionTimeout); + fail("Exception expected"); + } + catch (XAException ignored) { + } + verify(this.xaDataSource, times(1)).getXAConnection(anyString(), anyString()); + verify(this.xaConnection, times(0)).close(); + verify(this.xaConnection, times(0)).getXAResource(); + verify(this.xaResource, times(0)).getTransactionTimeout(); + assertThat(this.connectionManager.isConnected()).isFalse(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java index 95418983d455..2774610cc671 100644 --- a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java @@ -16,20 +16,18 @@ package org.springframework.boot.jta.narayana; -import java.sql.SQLException; - -import javax.sql.XAConnection; -import javax.sql.XADataSource; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -40,135 +38,124 @@ */ public class DataSourceXAResourceRecoveryHelperTests { - private XADataSource xaDataSource; - - private XAConnection xaConnection; - - private XAResource xaResource; + @Mock + private ConnectionManager connectionManager; private DataSourceXAResourceRecoveryHelper recoveryHelper; @Before - public void before() throws SQLException { - this.xaDataSource = mock(XADataSource.class); - this.xaConnection = mock(XAConnection.class); - this.xaResource = mock(XAResource.class); - this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.xaDataSource); - - given(this.xaDataSource.getXAConnection()).willReturn(this.xaConnection); - given(this.xaConnection.getXAResource()).willReturn(this.xaResource); + public void before() throws Exception { + MockitoAnnotations.initMocks(this); + + this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.connectionManager); } @Test - public void shouldCreateConnectionAndGetXAResource() throws SQLException { + public void shouldCreateConnectionAndGetXAResource() throws XAException { + given(this.connectionManager.isConnected()).willReturn(false); + XAResource[] xaResources = this.recoveryHelper.getXAResources(); + assertThat(xaResources.length).isEqualTo(1); assertThat(xaResources[0]).isSameAs(this.recoveryHelper); - verify(this.xaDataSource, times(1)).getXAConnection(); - verify(this.xaConnection, times(1)).getXAResource(); + verify(this.connectionManager, times(1)).isConnected(); + verify(this.connectionManager, times(1)).connect(); } @Test - public void shouldCreateConnectionWithCredentialsAndGetXAResource() - throws SQLException { - given(this.xaDataSource.getXAConnection(anyString(), anyString())) - .willReturn(this.xaConnection); - this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.xaDataSource, - "username", "password"); + public void shouldGetXAResourceWithoutConnecting() throws XAException { + given(this.connectionManager.isConnected()).willReturn(true); + XAResource[] xaResources = this.recoveryHelper.getXAResources(); + assertThat(xaResources.length).isEqualTo(1); assertThat(xaResources[0]).isSameAs(this.recoveryHelper); - verify(this.xaDataSource, times(1)).getXAConnection("username", "password"); - verify(this.xaConnection, times(1)).getXAResource(); + verify(this.connectionManager, times(1)).isConnected(); + verify(this.connectionManager, times(0)).connect(); } @Test - public void shouldFailToCreateConnectionAndNotGetXAResource() throws SQLException { - given(this.xaDataSource.getXAConnection()) - .willThrow(new SQLException("Test exception")); + public void shouldFailToCreateConnectionAndNotGetXAResource() throws XAException { + given(this.connectionManager.isConnected()).willReturn(false); + willThrow(new XAException("test")).given(this.connectionManager).connect(); + XAResource[] xaResources = this.recoveryHelper.getXAResources(); + assertThat(xaResources.length).isEqualTo(0); - verify(this.xaDataSource, times(1)).getXAConnection(); - verify(this.xaConnection, times(0)).getXAResource(); + verify(this.connectionManager, times(1)).isConnected(); + verify(this.connectionManager, times(1)).connect(); } @Test public void shouldDelegateRecoverCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.recover(XAResource.TMSTARTRSCAN); - verify(this.xaResource, times(1)).recover(XAResource.TMSTARTRSCAN); + verify(this.connectionManager, times(1)).connectAndApply(any()); + verify(this.connectionManager, times(0)).disconnect(); } @Test - public void shouldDelegateRecoverCallAndCloseConnection() - throws XAException, SQLException { - this.recoveryHelper.getXAResources(); + public void shouldDelegateRecoverCallAndCloseConnection() throws XAException { this.recoveryHelper.recover(XAResource.TMENDRSCAN); - verify(this.xaResource, times(1)).recover(XAResource.TMENDRSCAN); - verify(this.xaConnection, times(1)).close(); + verify(this.connectionManager, times(1)).connectAndApply(any()); + verify(this.connectionManager, times(1)).disconnect(); } @Test public void shouldDelegateStartCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.start(null, 0); - verify(this.xaResource, times(1)).start(null, 0); + verify(this.connectionManager, times(1)).connectAndAccept(any()); } @Test public void shouldDelegateEndCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.end(null, 0); - verify(this.xaResource, times(1)).end(null, 0); + verify(this.connectionManager, times(1)).connectAndAccept(any()); } @Test public void shouldDelegatePrepareCall() throws XAException { - this.recoveryHelper.getXAResources(); - this.recoveryHelper.prepare(null); - verify(this.xaResource, times(1)).prepare(null); + given(this.connectionManager.connectAndApply(any())).willReturn(10); + assertThat(this.recoveryHelper.prepare(null)).isEqualTo(10); + verify(this.connectionManager, times(1)).connectAndApply(any()); } @Test public void shouldDelegateCommitCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.commit(null, true); - verify(this.xaResource, times(1)).commit(null, true); + verify(this.connectionManager, times(1)).connectAndAccept(any()); } @Test public void shouldDelegateRollbackCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.rollback(null); - verify(this.xaResource, times(1)).rollback(null); + verify(this.connectionManager, times(1)).connectAndAccept(any()); } @Test public void shouldDelegateIsSameRMCall() throws XAException { - this.recoveryHelper.getXAResources(); - this.recoveryHelper.isSameRM(null); - verify(this.xaResource, times(1)).isSameRM(null); + given(this.connectionManager.connectAndApply(any())).willReturn(true); + assertThat(this.recoveryHelper.isSameRM(null)).isTrue(); + verify(this.connectionManager, times(1)).connectAndApply(any()); } @Test public void shouldDelegateForgetCall() throws XAException { - this.recoveryHelper.getXAResources(); this.recoveryHelper.forget(null); - verify(this.xaResource, times(1)).forget(null); + verify(this.connectionManager, times(1)).connectAndAccept(any()); } @Test public void shouldDelegateGetTransactionTimeoutCall() throws XAException { - this.recoveryHelper.getXAResources(); - this.recoveryHelper.getTransactionTimeout(); - verify(this.xaResource, times(1)).getTransactionTimeout(); + given(this.connectionManager.connectAndApply(any())).willReturn(10); + assertThat(this.recoveryHelper.getTransactionTimeout()).isEqualTo(10); + verify(this.connectionManager, times(1)).connectAndApply(any()); } @Test public void shouldDelegateSetTransactionTimeoutCall() throws XAException { - this.recoveryHelper.getXAResources(); - this.recoveryHelper.setTransactionTimeout(0); - verify(this.xaResource, times(1)).setTransactionTimeout(0); + given(this.connectionManager.connectAndApply(any())).willReturn(true); + assertThat(this.recoveryHelper.setTransactionTimeout(0)).isTrue(); + verify(this.connectionManager, times(1)).connectAndApply(any()); } } diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java index 634187dd55f2..2233eaf1ea47 100644 --- a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java @@ -63,7 +63,7 @@ public void wrapWithCredentials() { assertThat(wrapped).isInstanceOf(NarayanaDataSourceBean.class); verify(this.recoveryManager, times(1)).registerXAResourceRecoveryHelper( any(DataSourceXAResourceRecoveryHelper.class)); - verify(this.properties, times(2)).getRecoveryDbUser(); + verify(this.properties, times(1)).getRecoveryDbUser(); verify(this.properties, times(1)).getRecoveryDbPass(); }