/**
* 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.activemq.store.jdbc;
import java.io.File;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.jms.Connection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ft.SyncCreateDataSource;
import org.apache.activemq.util.IOHelper;
import org.apache.activemq.util.LeaseLockerIOExceptionHandler;
import org.apache.activemq.util.Wait;
import org.apache.derby.jdbc.EmbeddedDataSource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Test to see if the JDBCExceptionIOHandler will restart the transport connectors correctly after
* the underlying DB has been stopped and restarted
*
* see AMQ-4575
*/
public class JDBCIOExceptionHandlerTest {
private static final Logger LOG = LoggerFactory.getLogger(JDBCIOExceptionHandlerTest.class);
private static final String TRANSPORT_URL = "tcp://0.0.0.0:0";
private ActiveMQConnectionFactory factory;
private ReconnectingEmbeddedDataSource dataSource;
private BrokerService broker;
@After
public void stopDB() {
if (dataSource != null) {
dataSource.stopDB();
}
}
protected BrokerService createBroker(boolean withJMX) throws Exception {
return createBroker("localhost", withJMX, true, true);
}
protected BrokerService createBroker(String name, boolean withJMX, boolean leaseLocker, boolean startStopConnectors) throws Exception {
BrokerService broker = new BrokerService();
broker.setBrokerName(name);
broker.setUseJmx(withJMX);
JDBCPersistenceAdapter jdbc = new JDBCPersistenceAdapter();
EmbeddedDataSource embeddedDataSource = (EmbeddedDataSource) jdbc.getDataSource();
// create a wrapper to EmbeddedDataSource to allow the connection be
// reestablished to derby db
dataSource = new ReconnectingEmbeddedDataSource(new SyncCreateDataSource(embeddedDataSource));
jdbc.setDataSource(dataSource);
jdbc.setLockKeepAlivePeriod(1000l);
if (leaseLocker) {
LeaseDatabaseLocker leaseDatabaseLocker = new LeaseDatabaseLocker();
leaseDatabaseLocker.setHandleStartException(true);
leaseDatabaseLocker.setLockAcquireSleepInterval(2000l);
jdbc.setLocker(leaseDatabaseLocker);
}
broker.setPersistenceAdapter(jdbc);
LeaseLockerIOExceptionHandler ioExceptionHandler = new LeaseLockerIOExceptionHandler();
ioExceptionHandler.setResumeCheckSleepPeriod(1000l);
ioExceptionHandler.setStopStartConnectors(startStopConnectors);
broker.setIoExceptionHandler(ioExceptionHandler);
String connectionUri = broker.addConnector(TRANSPORT_URL).getPublishableConnectString();
factory = new ActiveMQConnectionFactory(connectionUri);
return broker;
}
/*
* run test without JMX enabled
*/
@Test
public void testRecoverWithOutJMX() throws Exception {
recoverFromDisconnectDB(false);
}
/*
* run test with JMX enabled
*/
@Test
public void testRecoverWithJMX() throws Exception {
recoverFromDisconnectDB(true);
}
@Test
public void testSlaveStoppedLease() throws Exception {
testSlaveStopped(true);
}
@Test
public void testSlaveStoppedDefault() throws Exception {
testSlaveStopped(false);
}
public void testSlaveStopped(final boolean lease) throws Exception {
final BrokerService master = createBroker("master", true, lease, false);
master.start();
master.waitUntilStarted();
final AtomicReference<BrokerService> slave = new AtomicReference<BrokerService>();
Thread slaveThread = new Thread() {
public void run() {
try {
BrokerService broker = new BrokerService();
broker.setBrokerName("slave");
JDBCPersistenceAdapter jdbc = new JDBCPersistenceAdapter();
jdbc.setDataSource(dataSource);
jdbc.setLockKeepAlivePeriod(1000l);
if (lease) {
LeaseDatabaseLocker leaseDatabaseLocker = new LeaseDatabaseLocker();
leaseDatabaseLocker.setHandleStartException(true);
leaseDatabaseLocker.setLockAcquireSleepInterval(2000l);
jdbc.setLocker(leaseDatabaseLocker);
}
broker.setPersistenceAdapter(jdbc);
LeaseLockerIOExceptionHandler ioExceptionHandler = new LeaseLockerIOExceptionHandler();
ioExceptionHandler.setResumeCheckSleepPeriod(1000l);
ioExceptionHandler.setStopStartConnectors(false);
broker.setIoExceptionHandler(ioExceptionHandler);
slave.set(broker);
broker.start();
} catch (Exception e) {
e.printStackTrace();
}
}
};
slaveThread.start();
Thread.sleep(5000);
dataSource.stopDB();
assertTrue("Master hasn't been stopped", Wait.waitFor(new Wait.Condition() {
@Override
public boolean isSatisified() throws Exception {
return master.isStopped();
}
}));
assertTrue("Slave hasn't been stopped", Wait.waitFor(new Wait.Condition() {
@Override
public boolean isSatisified() throws Exception {
return slave.get().isStopped();
}
}));
}
public void recoverFromDisconnectDB(boolean withJMX) throws Exception {
try {
broker = createBroker(withJMX);
broker.start();
broker.waitUntilStarted();
// broker started - stop db underneath it
dataSource.stopDB();
// wait - allow the leaselocker to kick the JDBCIOExceptionHandler
TimeUnit.SECONDS.sleep(3);
// check connector has shutdown
checkTransportConnectorStopped();
// restart db underneath
dataSource.restartDB();
Wait.waitFor(new Wait.Condition() {
@Override
public boolean isSatisified() throws Exception {
LOG.debug("*** checking connector to start...");
try {
checkTransportConnectorStarted();
return true;
} catch (Throwable t) {
LOG.debug(t.toString());
}
return false;
}
});
} finally {
LOG.debug("*** broker is stopping...");
broker.stop();
}
}
private void checkTransportConnectorStopped() {
// connection is expected to fail
try {
factory.createConnection();
fail("Transport connector should be stopped");
} catch (Exception ex) {
// expected an exception
LOG.debug(" checkTransportConnectorStopped() threw", ex);
}
}
private void checkTransportConnectorStarted() {
// connection is expected to succeed
try {
Connection conn = factory.createConnection();
conn.close();
} catch (Exception ex) {
LOG.debug("checkTransportConnectorStarted() threw", ex);
fail("Transport connector should have been started");
}
}
/*
* Wrapped the derby datasource object to get DB reconnect functionality as I not
* manage to get that working directly on the EmbeddedDataSource
*
*/
public class ReconnectingEmbeddedDataSource implements javax.sql.DataSource {
private SyncCreateDataSource realDatasource;
public ReconnectingEmbeddedDataSource(SyncCreateDataSource datasource) {
this.realDatasource = datasource;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return this.realDatasource.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
this.realDatasource.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
this.realDatasource.setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return this.realDatasource.getLoginTimeout();
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return this.unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return this.isWrapperFor(iface);
}
@Override
public java.sql.Connection getConnection() throws SQLException {
return this.realDatasource.getConnection();
}
@Override
public java.sql.Connection getConnection(String username, String password) throws SQLException {
return this.getConnection(username, password);
}
/**
*
* To simulate a db reconnect I just create a new EmbeddedDataSource .
*
* @throws SQLException
*/
public void restartDB() throws Exception {
EmbeddedDataSource newDatasource =
(EmbeddedDataSource) DataSourceServiceSupport.createDataSource(broker.getDataDirectoryFile().getCanonicalPath());
newDatasource.getConnection();
LOG.info("*** DB restarted now...");
Object existingDataSource = realDatasource;
synchronized (existingDataSource) {
this.realDatasource = new SyncCreateDataSource(newDatasource);
}
}
public void stopDB() {
LOG.info("***DB is being shutdown...");
synchronized (realDatasource) {
DataSourceServiceSupport.shutdownDefaultDataSource(realDatasource.getDelegate());
}
}
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
}