/*
* JBoss, Home of Professional Open Source.
* Copyright 2016, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.test.integration.jca.flushing;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ReflectPermission;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.jca.adapters.jdbc.WrappedConnection;
import org.jboss.remoting3.security.RemotingPermission;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.jboss.as.controller.client.helpers.ClientConstants.ADD;
import static org.jboss.as.controller.client.helpers.ClientConstants.OP;
import static org.jboss.as.controller.client.helpers.ClientConstants.OP_ADDR;
import static org.jboss.as.controller.client.helpers.ClientConstants.OUTCOME;
import static org.jboss.as.controller.client.helpers.ClientConstants.REMOVE_OPERATION;
import static org.jboss.as.controller.client.helpers.ClientConstants.SUBSYSTEM;
import static org.jboss.as.controller.client.helpers.ClientConstants.SUCCESS;
import static org.jboss.as.test.shared.integration.ejb.security.PermissionUtils.createPermissionsXmlAsset;
/**
* Test for flush operations on data source pools:
* - flush-all-connection-in-pool
* - flush-idle-connection-in-pool
* - flush-invalid-connection-in-pool
* - flush-gracefully-connection-in-pool
* <p>
* Everything is tested with allow-multiple-users=true.
* <p>
* For testing whether a pooled connection is really closed, we check whether the underlying connection is closed.
*
* @author Jan Martiska
*/
@RunWith(Arquillian.class)
public class FlushOperationsTestCase {
@Deployment
public static JavaArchive deployment() {
final JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "flush-operations.jar");
archive.addClass(FlushOperationsTestCase.class);
archive.addAsManifestResource(
new StringAsset(
"Dependencies: org.jboss.as.controller-client, "
+ "org.jboss.as.controller, "
+ "org.jboss.dmr, "
+ "org.jboss.ironjacamar.jdbcadapters, "
+ "org.jboss.remoting\n"),
"MANIFEST.MF");
archive.addAsManifestResource(createPermissionsXmlAsset(
// ModelControllerClient needs the following
new RemotingPermission("createEndpoint"),
new RemotingPermission("connect"),
// flushInvalidConnectionsInPool needs the following
new RuntimePermission("accessDeclaredMembers"),
new ReflectPermission("suppressAccessChecks")
), "permissions.xml");
return archive;
}
@ArquillianResource
private ManagementClient managementClient;
private PathAddress dsAddress;
private String dsName;
private String dsJndiName;
private DataSource dataSource;
private List<Connection> connectionList;
private List<Connection> physicalConnectionList;
@Before
public void createDatasource() throws Exception {
dsName = java.util.UUID.randomUUID().toString();
dsAddress = PathAddress.pathAddress(SUBSYSTEM, "datasources").append("data-source", dsName);
dsJndiName = "java:/" + dsName;
final ModelNode createDsOp = new ModelNode();
createDsOp.get(OP).set(ADD);
createDsOp.get(OP_ADDR).set(dsAddress.toModelNode());
createDsOp.get("jndi-name").set(dsJndiName);
createDsOp.get("valid-connection-checker-class-name")
.set("org.jboss.jca.adapters.jdbc.extensions.novendor.JDBC4ValidConnectionChecker");
createDsOp.get("driver-name").set("h2");
createDsOp.get("allow-multiple-users").set(true);
createDsOp.get("connection-url").set("jdbc:h2:mem:test42");
executeAndAssertSuccess(createDsOp);
dataSource = getDataSourceInstanceFromJndi();
}
/**
* After each test:
* - close all connections in case that some remained open
* - delete the data source
*/
@After
public void cleanup() throws Exception {
if (connectionList != null) {
connectionList.forEach(this::closeIfNecessary);
connectionList = null;
}
final ModelNode removeOp = new ModelNode();
removeOp.get(OP).set(REMOVE_OPERATION);
removeOp.get(OP_ADDR).set(dsAddress.toModelNode());
executeAndAssertSuccess(removeOp);
}
/**
* Obtain 6 connections.
* Return connections 0,1,2 to the pool.
* Flush all connections.
* All connections, especially 3,4,5; should be closed after the flush.
*/
@Test
public void flushAllConnectionsInPool() throws NamingException, SQLException, IOException {
initConnections(6);
connectionList.get(0).close();
connectionList.get(1).close();
connectionList.get(2).close();
runDataSourceOperationAndAssertSuccess("flush-all-connection-in-pool");
for (int i = 0; i < connectionList.size(); i++) {
Assert.assertTrue("Connection #" + i + " should be flushed",
physicalConnectionList.get(i).isClosed());
}
}
/**
* Obtain 2 connections.
* Return connections 0 to the pool.
* Flush all connections gracefully.
* Check that only connections 0 is closed while 1 remains open.
* Return connections 3,4,5 to the pool.
* Check all connections 3,4,5 were closed now.
*/
@Test
public void flushGracefullyConnectionsInPool() throws Exception {
initConnections(6);
connectionList.get(0).close();
connectionList.get(1).close();
connectionList.get(2).close();
runDataSourceOperationAndAssertSuccess("flush-gracefully-connection-in-pool");
Assert.assertTrue("Connection 0 should get destroyed immediately because it is idle",
physicalConnectionList.get(0).isClosed());
Assert.assertTrue("Connection 1 should get destroyed immediately because it is idle",
physicalConnectionList.get(1).isClosed());
Assert.assertTrue("Connection 2 should get destroyed immediately because it is idle",
physicalConnectionList.get(2).isClosed());
Assert.assertFalse("Connection 3 should not get destroyed immediately because it is not idle",
physicalConnectionList.get(3).isClosed());
Assert.assertFalse("Connection 4 should not get destroyed immediately because it is not idle",
physicalConnectionList.get(4).isClosed());
Assert.assertFalse("Connection 5 should not get destroyed immediately because it is not idle",
physicalConnectionList.get(5).isClosed());
connectionList.get(3).close();
connectionList.get(4).close();
connectionList.get(5).close();
for (int i = 0; i < connectionList.size(); i++) {
Assert.assertTrue("Connection #" + i + " should be flushed",
physicalConnectionList.get(i).isClosed());
}
}
/**
* Obtain 6 connections.
* Return connections 0,1,2 to the pool.
* Run flush-idle-connections-in-pool
* Check that only the physical connections corresponding to handles 0,1,2 were destroyed.
*/
@Test
public void flushIdleConnectionsInPool() throws Exception {
initConnections(6);
connectionList.get(0).close();
connectionList.get(1).close();
connectionList.get(2).close();
runDataSourceOperationAndAssertSuccess("flush-idle-connection-in-pool");
Assert.assertTrue("Connection 0 should get destroyed because it is idle",
physicalConnectionList.get(0).isClosed());
Assert.assertTrue("Connection 1 should get destroyed because it is idle",
physicalConnectionList.get(1).isClosed());
Assert.assertTrue("Connection 2 should get destroyed because it is idle",
physicalConnectionList.get(2).isClosed());
Assert.assertFalse("Connection 3 should not get destroyed because it is not idle",
physicalConnectionList.get(3).isClosed());
Assert.assertFalse("Connection 4 should not get destroyed because it is not idle",
physicalConnectionList.get(4).isClosed());
Assert.assertFalse("Connection 5 should not get destroyed because it is not idle",
physicalConnectionList.get(5).isClosed());
}
/**
* Obtain 3 connections.
* Return connections 0,1.
* Mark connection 0 as invalid
* Run flush-invalid-connection-in-pool
* Managed connection of connection 0 should be flushed
* Managed connection of connection 1 should NOT be flushed because the connection was not marked invalid
* Managed connection of connection 2 should NOT be flushed because the connection was not idle
*/
@Test
public void flushInvalidConnectionsInPool() throws Exception {
initConnections(3);
connectionList.get(0).close();
connectionList.get(1).close();
// make connection 0 invalid somehow - for example, hack its session field to null
// this only works with H2
final Field sessionField = physicalConnectionList.get(0).getClass().getDeclaredField("session");
try {
sessionField.setAccessible(true);
sessionField.set(physicalConnectionList.get(0), null);
} finally {
sessionField.setAccessible(false);
}
runDataSourceOperationAndAssertSuccess("flush-invalid-connection-in-pool");
Assert.assertTrue("Connection 0 should get destroyed because it is marked invalid",
physicalConnectionList.get(0).isClosed());
Assert.assertFalse("Connection 1 should not get destroyed because it is valid",
physicalConnectionList.get(1).isClosed());
Assert.assertFalse("Connection 2 should not get destroyed because it is not idle",
physicalConnectionList.get(2).isClosed());
}
/**
* Creates a number of connections using the tested data source.
* Stores the connections into a list for reference.
*/
public void initConnections(int count) {
connectionList = IntStream.range(0, count)
.mapToObj(i -> connectionSupplier.apply(dataSource))
.collect(Collectors.toList());
physicalConnectionList = connectionList
.stream()
.map(this::getUnderlyingConnection)
.collect(Collectors.toList());
}
public void executeAndAssertSuccess(ModelNode op) throws IOException {
final ModelNode result = managementClient.getControllerClient().execute(op);
if (!result.get(OUTCOME).asString().equals(SUCCESS)) {
Assert.fail("Operation failed: " + result.toJSONString(true));
}
}
public void runDataSourceOperationAndAssertSuccess(String operationName) throws IOException {
final ModelNode op = new ModelNode();
op.get(OP).set(operationName);
op.get(OP_ADDR).set(dsAddress.toModelNode());
executeAndAssertSuccess(op);
}
public DataSource getDataSourceInstanceFromJndi() throws NamingException {
return (DataSource)new InitialContext().lookup(dsJndiName);
}
public Function<DataSource, Connection> connectionSupplier = (dataSource -> {
try {
return dataSource.getConnection("sa", "");
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public void closeIfNecessary(Connection connection) {
try {
if (!connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getUnderlyingConnection(Connection handle) {
try {
return ((WrappedConnection)handle).getUnderlyingConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}