/*
* 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.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Pair;
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.core.client.impl.ClientSessionFactoryInternal;
import org.apache.activemq.artemis.core.client.impl.ServerLocatorInternal;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.journal.impl.JournalFile;
import org.apache.activemq.artemis.core.journal.impl.JournalImpl;
import org.apache.activemq.artemis.core.paging.PagingStore;
import org.apache.activemq.artemis.core.persistence.impl.journal.DescribeJournal;
import org.apache.activemq.artemis.core.persistence.impl.journal.JournalStorageManager;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.core.server.files.FileMoveManager;
import org.apache.activemq.artemis.tests.integration.cluster.util.BackupSyncDelay;
import org.apache.activemq.artemis.tests.integration.cluster.util.TestableServer;
import org.apache.activemq.artemis.tests.util.TransportConfigurationUtils;
import org.apache.activemq.artemis.utils.UUID;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class BackupSyncJournalTest extends FailoverTestBase {
private static final Logger logger = Logger.getLogger(BackupSyncJournalTest.class);
protected static final int BACKUP_WAIT_TIME = 60;
private ServerLocatorInternal locator;
protected ClientSessionFactoryInternal sessionFactory;
protected ClientSession session;
protected ClientProducer producer;
private BackupSyncDelay syncDelay;
private final int defaultNMsgs = 20;
private int n_msgs = defaultNMsgs;
protected void setNumberOfMessages(int nmsg) {
this.n_msgs = nmsg;
}
protected int getNumberOfMessages() {
return n_msgs;
}
@Override
@Before
public void setUp() throws Exception {
startBackupServer = false;
super.setUp();
setNumberOfMessages(defaultNMsgs);
locator = (ServerLocatorInternal) getServerLocator().setBlockOnNonDurableSend(true).setBlockOnDurableSend(true).setReconnectAttempts(-1);
sessionFactory = createSessionFactoryAndWaitForTopology(locator, 1);
syncDelay = new BackupSyncDelay(backupServer, liveServer);
}
@Test
public void testNodeID() throws Exception {
startBackupFinishSyncing();
assertTrue("must be running", backupServer.isStarted());
assertEquals("backup and live should have the same nodeID", liveServer.getServer().getNodeID(), backupServer.getServer().getNodeID());
}
@Test
public void testReserveFileIdValuesOnBackup() throws Exception {
final int totalRounds = 5;
createProducerSendSomeMessages();
JournalImpl messageJournal = getMessageJournalFromServer(liveServer);
for (int i = 0; i < totalRounds; i++) {
messageJournal.forceMoveNextFile();
sendMessages(session, producer, n_msgs);
}
Queue queue = liveServer.getServer().locateQueue(ADDRESS);
PagingStore store = queue.getPageSubscription().getPagingStore();
// in case of paging I must close the current page otherwise we will get a pending counter
// what would make the verification on similar journal to fail after the recovery
if (store.isPaging()) {
store.forceAnotherPage();
}
backupServer.start();
// Deliver messages with Backup in-sync
waitForRemoteBackup(sessionFactory, BACKUP_WAIT_TIME, false, backupServer.getServer());
final JournalImpl backupMsgJournal = getMessageJournalFromServer(backupServer);
sendMessages(session, producer, n_msgs);
// in case of paging I must close the current page otherwise we will get a pending counter
// what would make the verification on similar journal to fail after the recovery
if (store.isPaging()) {
store.forceAnotherPage();
}
// Deliver messages with Backup up-to-date
syncDelay.deliverUpToDateMsg();
waitForRemoteBackup(sessionFactory, BACKUP_WAIT_TIME, true, backupServer.getServer());
// SEND more messages, now with the backup replicating
sendMessages(session, producer, n_msgs);
// in case of paging I must close the current page otherwise we will get a pending counter
// what would make the verification on similar journal to fail after the recovery
if (store.isPaging()) {
store.forceAnotherPage();
}
Set<Pair<Long, Integer>> liveIds = getFileIds(messageJournal);
int size = messageJournal.getFileSize();
PagingStore ps = liveServer.getServer().getPagingManager().getPageStore(ADDRESS);
if (ps.getPageSizeBytes() == PAGE_SIZE) {
assertTrue("isStarted", ps.isStarted());
assertFalse("start paging should return false, because we expect paging to be running", ps.startPaging());
}
finishSyncAndFailover();
assertEquals("file sizes must be the same", size, backupMsgJournal.getFileSize());
Set<Pair<Long, Integer>> backupIds = getFileIds(backupMsgJournal);
int total = 0;
for (Pair<Long, Integer> pair : liveIds) {
total += pair.getB();
}
int totalBackup = 0;
for (Pair<Long, Integer> pair : backupIds) {
totalBackup += pair.getB();
}
assertEquals("number of records must match ", total, totalBackup);
// "+ 2": there two other calls that send N_MSGS.
for (int i = 0; i < totalRounds + 3; i++) {
receiveMsgsInRange(0, n_msgs);
}
assertNoMoreMessages();
}
protected void assertNoMoreMessages() throws ActiveMQException {
session.start();
ClientConsumer consumer = session.createConsumer(ADDRESS);
ClientMessage msg = consumer.receiveImmediate();
assertNull("there should be no more messages to receive! " + msg, msg);
consumer.close();
session.commit();
}
protected void startBackupFinishSyncing() throws Exception {
syncDelay.deliverUpToDateMsg();
backupServer.start();
waitForRemoteBackup(sessionFactory, BACKUP_WAIT_TIME, true, backupServer.getServer());
}
@Test
public void testReplicationDuringSync() throws Exception {
try {
createProducerSendSomeMessages();
backupServer.start();
waitForRemoteBackup(sessionFactory, BACKUP_WAIT_TIME, false, backupServer.getServer());
sendMessages(session, producer, n_msgs);
session.commit();
receiveMsgsInRange(0, n_msgs);
finishSyncAndFailover();
receiveMsgsInRange(0, n_msgs);
assertNoMoreMessages();
} catch (AssertionError error) {
printJournal(liveServer);
printJournal(backupServer);
// test failed
throw error;
}
}
void printJournal(TestableServer server) {
try {
System.out.println("\n\n BINDINGS JOURNAL\n\n");
Configuration config = server.getServer().getConfiguration();
DescribeJournal.describeBindingsJournal(config.getBindingsLocation());
System.out.println("\n\n MESSAGES JOURNAL\n\n");
DescribeJournal.describeMessagesJournal(config.getJournalLocation());
} catch (Exception ignored) {
ignored.printStackTrace();
}
}
protected void finishSyncAndFailover() throws Exception {
syncDelay.deliverUpToDateMsg();
waitForRemoteBackup(sessionFactory, BACKUP_WAIT_TIME, true, backupServer.getServer());
assertFalse("should not be initialized", backupServer.getServer().isActive());
crash(session);
assertTrue("backup initialized", backupServer.getServer().waitForActivation(5, TimeUnit.SECONDS));
assertNodeIdWasSaved();
}
/**
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
* @throws InterruptedException
*/
private void assertNodeIdWasSaved() throws Exception {
assertTrue("backup initialized", backupServer.getServer().waitForActivation(5, TimeUnit.SECONDS));
// assert that nodeID was saved (to the right file!)
String journalDirectory = backupConfig.getJournalDirectory();
File serverLockFile = new File(journalDirectory, "server.lock");
assertTrue("server.lock must exist!\n " + serverLockFile, serverLockFile.exists());
RandomAccessFile raFile = new RandomAccessFile(serverLockFile, "r");
try {
// verify the nodeID was written correctly
FileChannel channel = raFile.getChannel();
final int size = 16;
ByteBuffer id = ByteBuffer.allocateDirect(size);
int read = channel.read(id, 3);
assertEquals("tried to read " + size + " bytes", size, read);
byte[] bytes = new byte[16];
id.position(0);
id.get(bytes);
UUID uuid = new UUID(UUID.TYPE_TIME_BASED, bytes);
SimpleString storedNodeId = new SimpleString(uuid.toString());
assertEquals("nodeId must match", backupServer.getServer().getNodeID(), storedNodeId);
} finally {
raFile.close();
}
}
@Test
public void testMessageSyncSimple() throws Exception {
createProducerSendSomeMessages();
startBackupCrashLive();
receiveMsgsInRange(0, n_msgs);
assertNoMoreMessages();
}
/**
* Basic fail-back test.
*
* @throws Exception
*/
@Test
public void testFailBack() throws Exception {
createProducerSendSomeMessages();
startBackupCrashLive();
receiveMsgsInRange(0, n_msgs);
assertNoMoreMessages();
sendMessages(session, producer, n_msgs);
receiveMsgsInRange(0, n_msgs);
assertNoMoreMessages();
sendMessages(session, producer, 2 * n_msgs);
assertFalse("must NOT be a backup", liveServer.getServer().getHAPolicy().isBackup());
adaptLiveConfigForReplicatedFailBack(liveServer);
FileMoveManager liveMoveManager = new FileMoveManager(liveServer.getServer().getConfiguration().getJournalLocation(), -1);
liveServer.getServer().lockActivation();
try {
liveServer.start();
assertTrue("must have become a backup", liveServer.getServer().getHAPolicy().isBackup());
Assert.assertEquals(0, liveMoveManager.getNumberOfFolders());
} finally {
liveServer.getServer().unlockActivation();
}
waitForServerToStart(liveServer.getServer());
liveServer.getServer().waitForActivation(10, TimeUnit.SECONDS);
Assert.assertEquals(1, liveMoveManager.getNumberOfFolders());
assertTrue("must be active now", !liveServer.getServer().getHAPolicy().isBackup());
assertTrue("Fail-back must initialize live!", liveServer.getServer().waitForActivation(15, TimeUnit.SECONDS));
assertFalse("must be LIVE!", liveServer.getServer().getHAPolicy().isBackup());
int i = 0;
while (!backupServer.isStarted() && i++ < 100) {
Thread.sleep(100);
}
assertTrue(backupServer.getServer().isStarted());
assertTrue(liveServer.getServer().isStarted());
receiveMsgsInRange(0, 2 * n_msgs);
assertNoMoreMessages();
}
@Test
public void testMessageSync() throws Exception {
createProducerSendSomeMessages();
receiveMsgsInRange(0, n_msgs / 2);
startBackupCrashLive();
receiveMsgsInRange(n_msgs / 2, n_msgs);
assertNoMoreMessages();
}
private void startBackupCrashLive() throws Exception {
assertFalse("backup is started?", backupServer.isStarted());
liveServer.removeInterceptor(syncDelay);
backupServer.start();
waitForBackup(sessionFactory, BACKUP_WAIT_TIME);
crash(session);
backupServer.getServer().waitForActivation(5, TimeUnit.SECONDS);
}
protected void createProducerSendSomeMessages() throws ActiveMQException {
session = addClientSession(sessionFactory.createSession(true, true));
session.createQueue(ADDRESS, RoutingType.MULTICAST, ADDRESS, null, true);
if (producer != null)
producer.close();
producer = addClientProducer(session.createProducer(ADDRESS));
sendMessages(session, producer, n_msgs);
session.commit();
}
protected void receiveMsgsInRange(int start, int end) throws ActiveMQException {
session.start();
ClientConsumer consumer = addClientConsumer(session.createConsumer(ADDRESS));
receiveMessages(consumer, start, end, true);
consumer.close();
session.commit();
}
private Set<Pair<Long, Integer>> getFileIds(JournalImpl journal) {
Set<Pair<Long, Integer>> results = new HashSet<>();
for (JournalFile jf : journal.getDataFiles()) {
results.add(getPair(jf));
}
results.add(getPair(journal.getCurrentFile()));
return results;
}
/**
* @param jf
* @return
*/
private Pair<Long, Integer> getPair(JournalFile jf) {
return new Pair<>(jf.getFileID(), jf.getPosCount());
}
static JournalImpl getMessageJournalFromServer(TestableServer server) {
JournalStorageManager sm = (JournalStorageManager) server.getServer().getStorageManager();
return (JournalImpl) sm.getMessageJournal();
}
@Override
protected void createConfigs() throws Exception {
createReplicatedConfigs();
}
@Override
protected TransportConfiguration getAcceptorTransportConfiguration(boolean live) {
return TransportConfigurationUtils.getInVMAcceptor(live);
}
@Override
protected TransportConfiguration getConnectorTransportConfiguration(boolean live) {
return TransportConfigurationUtils.getInVMConnector(live);
}
}