/*
* 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.artemis.tests.integration.cluster.failover;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.ActiveMQDuplicateIdException;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQTransactionOutcomeUnknownException;
import org.apache.activemq.artemis.api.core.ActiveMQTransactionRolledBackException;
import org.apache.activemq.artemis.api.core.ActiveMQUnBlockedException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ClientConsumer;
import org.apache.activemq.artemis.api.core.client.ClientMessage;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryInternal;
import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.tests.integration.IntegrationTestLogger;
import org.apache.activemq.artemis.tests.util.CountDownSessionFailureListener;
import org.apache.activemq.artemis.tests.util.TransportConfigurationUtils;
import org.junit.Assert;
import org.junit.Test;
/**
* A MultiThreadFailoverTest
* <br>
* Test Failover where failure is prompted by another thread
*/
public class AsynchronousFailoverTest extends FailoverTestBase {
private static final IntegrationTestLogger log = IntegrationTestLogger.LOGGER;
private volatile CountDownSessionFailureListener listener;
private volatile ClientSessionFactoryInternal sf;
private final Object lockFail = new Object();
@Test
public void testNonTransactional() throws Throwable {
runTest(new TestRunner() {
@Override
public void run() {
try {
doTestNonTransactional(this);
} catch (Throwable e) {
AsynchronousFailoverTest.log.error("Test failed", e);
addException(e);
}
}
});
}
@Test
public void testTransactional() throws Throwable {
runTest(new TestRunner() {
volatile boolean running = false;
@Override
public void run() {
try {
assertFalse(running);
running = true;
try {
doTestTransactional(this);
} finally {
running = false;
}
} catch (Throwable e) {
AsynchronousFailoverTest.log.error("Test failed", e);
addException(e);
}
}
});
}
abstract class TestRunner implements Runnable {
volatile boolean failed;
ArrayList<Throwable> errors = new ArrayList<>();
boolean isFailed() {
return failed;
}
void setFailed() {
failed = true;
}
void reset() {
failed = false;
}
synchronized void addException(Throwable e) {
errors.add(e);
}
void checkForExceptions() throws Throwable {
if (errors.size() > 0) {
log.warn("Exceptions on test:");
for (Throwable e : errors) {
log.warn(e.getMessage(), e);
}
// throwing the first error that happened on the Runnable
throw errors.get(0);
}
}
}
private void runTest(final TestRunner runnable) throws Throwable {
final int numIts = 1;
try {
for (int i = 0; i < numIts; i++) {
AsynchronousFailoverTest.log.info("Iteration " + i);
ServerLocator locator = getServerLocator().setBlockOnNonDurableSend(true).setBlockOnDurableSend(true).setReconnectAttempts(-1).setConfirmationWindowSize(10 * 1024 * 1024);
sf = createSessionFactoryAndWaitForTopology(locator, 2);
try {
ClientSession createSession = sf.createSession(true, true);
createSession.createQueue(FailoverTestBase.ADDRESS, FailoverTestBase.ADDRESS, (SimpleString) null, true);
RemotingConnection conn = ((ClientSessionInternal) createSession).getConnection();
Thread t = new Thread(runnable);
t.setName("MainTEST");
t.start();
long randomDelay = (long) (2000 * Math.random());
AsynchronousFailoverTest.log.info("Sleeping " + randomDelay);
Thread.sleep(randomDelay);
AsynchronousFailoverTest.log.info("Failing asynchronously");
// Simulate failure on connection
synchronized (lockFail) {
if (log.isDebugEnabled()) {
log.debug("#test crashing test");
}
crash(createSession);
}
/*if (listener != null)
{
boolean ok = listener.latch.await(10000, TimeUnit.MILLISECONDS);
Assert.assertTrue(ok);
}*/
runnable.setFailed();
AsynchronousFailoverTest.log.info("Fail complete");
t.join(TimeUnit.SECONDS.toMillis(60));
if (t.isAlive()) {
System.out.println(threadDump("Thread still running from the test"));
t.interrupt();
fail("Test didn't complete successful, thread still running");
}
runnable.checkForExceptions();
createSession.close();
Assert.assertEquals(0, sf.numSessions());
locator.close();
} finally {
locator.close();
Assert.assertEquals(0, sf.numConnections());
}
if (i != numIts - 1) {
tearDown();
runnable.checkForExceptions();
runnable.reset();
setUp();
}
}
} finally {
}
}
protected void addPayload(ClientMessage msg) {
}
private void doTestNonTransactional(final TestRunner runner) throws Exception {
while (!runner.isFailed()) {
AsynchronousFailoverTest.log.info("looping");
ClientSession session = sf.createSession(true, true, 0);
listener = new CountDownSessionFailureListener(session);
session.addFailureListener(listener);
ClientProducer producer = session.createProducer(FailoverTestBase.ADDRESS);
final int numMessages = 1000;
for (int i = 0; i < numMessages; i++) {
boolean retry = false;
do {
try {
ClientMessage message = session.createMessage(true);
message.getBodyBuffer().writeString("message" + i);
message.putIntProperty("counter", i);
addPayload(message);
producer.send(message);
retry = false;
} catch (ActiveMQUnBlockedException ube) {
AsynchronousFailoverTest.log.info("exception when sending message with counter " + i);
ube.printStackTrace();
retry = true;
} catch (ActiveMQException e) {
fail("Invalid Exception type:" + e.getType());
}
} while (retry);
}
// create the consumer with retry if failover occurs during createConsumer call
ClientConsumer consumer = null;
boolean retry = false;
do {
try {
consumer = session.createConsumer(FailoverTestBase.ADDRESS);
retry = false;
} catch (ActiveMQUnBlockedException ube) {
AsynchronousFailoverTest.log.info("exception when creating consumer");
retry = true;
} catch (ActiveMQException e) {
fail("Invalid Exception type:" + e.getType());
}
} while (retry);
session.start();
List<Integer> counts = new ArrayList<>(1000);
int lastCount = -1;
boolean counterGap = false;
while (true) {
ClientMessage message = consumer.receive(500);
if (message == null) {
break;
}
// messages must remain ordered but there could be a "jump" if messages
// are missing or duplicated
int count = message.getIntProperty("counter");
counts.add(count);
if (count != lastCount + 1) {
if (counterGap) {
Assert.fail("got another counter gap at " + count + ": " + counts);
} else {
if (lastCount != -1) {
AsynchronousFailoverTest.log.info("got first counter gap at " + count);
counterGap = true;
}
}
}
lastCount = count;
message.acknowledge();
}
session.close();
this.listener = null;
}
}
private void doTestTransactional(final TestRunner runner) throws Throwable {
// For duplication detection
int executionId = 0;
while (!runner.isFailed()) {
ClientSession session = null;
executionId++;
log.info("#test doTestTransactional starting now. Execution " + executionId);
try {
boolean retry = false;
final int numMessages = 1000;
session = sf.createSession(false, false);
listener = new CountDownSessionFailureListener(session);
session.addFailureListener(listener);
do {
try {
ClientProducer producer = session.createProducer(FailoverTestBase.ADDRESS);
for (int i = 0; i < numMessages; i++) {
ClientMessage message = session.createMessage(true);
message.getBodyBuffer().writeString("message" + i);
message.putIntProperty("counter", i);
message.putStringProperty(Message.HDR_DUPLICATE_DETECTION_ID, new SimpleString("id:" + i +
",exec:" +
executionId));
addPayload(message);
if (log.isDebugEnabled()) {
log.debug("Sending message " + message);
}
producer.send(message);
}
log.debug("Sending commit");
session.commit();
retry = false;
} catch (ActiveMQDuplicateIdException die) {
logAndSystemOut("#test duplicate id rejected on sending");
break;
} catch (ActiveMQTransactionRolledBackException trbe) {
log.info("#test transaction rollback retrying on sending");
// OK
retry = true;
} catch (ActiveMQUnBlockedException ube) {
log.info("#test transaction rollback retrying on sending");
// OK
retry = true;
} catch (ActiveMQTransactionOutcomeUnknownException toue) {
log.info("#test transaction rollback retrying on sending");
// OK
retry = true;
} catch (ActiveMQException e) {
log.info("#test Exception " + e, e);
throw e;
}
} while (retry);
logAndSystemOut("#test Finished sending, starting consumption now");
boolean blocked = false;
retry = false;
ClientConsumer consumer = null;
do {
ArrayList<Integer> msgs = new ArrayList<>();
try {
if (consumer == null) {
consumer = session.createConsumer(FailoverTestBase.ADDRESS);
session.start();
}
for (int i = 0; i < numMessages; i++) {
if (log.isDebugEnabled()) {
log.debug("Consumer receiving message " + i);
}
ClientMessage message = consumer.receive(10000);
if (message == null) {
break;
}
if (log.isDebugEnabled()) {
log.debug("Received message " + message);
}
int count = message.getIntProperty("counter");
if (count != i) {
log.warn("count was received out of order, " + count + "!=" + i);
}
msgs.add(count);
message.acknowledge();
}
log.info("#test commit");
try {
session.commit();
} catch (ActiveMQTransactionRolledBackException trbe) {
//we know the tx has been rolled back so we just consume again
retry = true;
continue;
} catch (ActiveMQException e) {
// This could eventually happen
// We will get rid of this when we implement 2 phase commit on failover
log.warn("exception during commit, it will be ignored for now" + e.getMessage(), e);
}
try {
if (blocked) {
assertTrue("msgs.size is expected to be 0 or " + numMessages + " but it was " + msgs.size(), msgs.size() == 0 || msgs.size() == numMessages);
} else {
assertTrue("msgs.size is expected to be " + numMessages + " but it was " + msgs.size(), msgs.size() == numMessages);
}
} catch (Throwable e) {
log.info(threadDump("Thread dump, messagesReceived = " + msgs.size()));
logAndSystemOut(e.getMessage() + " messages received");
for (Integer msg : msgs) {
logAndSystemOut(msg.toString());
}
throw e;
}
int i = 0;
for (Integer msg : msgs) {
assertEquals(i++, (int) msg);
}
retry = false;
blocked = false;
} catch (ActiveMQTransactionRolledBackException trbe) {
logAndSystemOut("Transaction rolled back with " + msgs.size(), trbe);
// TODO: https://jira.jboss.org/jira/browse/HORNETQ-369
// ATM RolledBack exception is being called with the transaction is committed.
// the test will fail if you remove this next line
blocked = true;
retry = true;
} catch (ActiveMQTransactionOutcomeUnknownException tou) {
logAndSystemOut("Transaction rolled back with " + msgs.size(), tou);
// TODO: https://jira.jboss.org/jira/browse/HORNETQ-369
// ATM RolledBack exception is being called with the transaction is committed.
// the test will fail if you remove this next line
blocked = true;
retry = true;
} catch (ActiveMQUnBlockedException ube) {
logAndSystemOut("Unblocked with " + msgs.size(), ube);
// TODO: https://jira.jboss.org/jira/browse/HORNETQ-369
// This part of the test is never being called.
blocked = true;
retry = true;
} catch (ActiveMQException e) {
logAndSystemOut(e.getMessage(), e);
throw e;
}
} while (retry);
} finally {
if (session != null) {
session.close();
}
}
listener = null;
}
}
@Override
protected TransportConfiguration getAcceptorTransportConfiguration(final boolean live) {
return TransportConfigurationUtils.getInVMAcceptor(live);
}
@Override
protected TransportConfiguration getConnectorTransportConfiguration(final boolean live) {
return TransportConfigurationUtils.getInVMConnector(live);
}
}