/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2013-2015 ForgeRock AS
* Portions Copyright 2014 ForgeRock AS
*/
package org.opends.server.replication.server.changelog.file;
import java.lang.Thread.State;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.ChangelogState;
import org.opends.server.replication.server.changelog.api.ChangeNumberIndexDB;
import org.opends.server.replication.server.changelog.api.ChangeNumberIndexRecord;
import org.opends.server.replication.server.changelog.api.ChangelogDB;
import org.opends.server.replication.server.changelog.api.ChangelogException;
import org.opends.server.replication.server.changelog.api.DBCursor.CursorOptions;
import org.opends.server.replication.server.changelog.api.ReplicaId;
import org.opends.server.replication.server.changelog.api.ReplicationDomainDB;
import org.opends.server.replication.server.changelog.api.ChangelogStateProvider;
import org.opends.server.types.DN;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import static org.opends.server.replication.server.changelog.api.DBCursor.KeyMatchingStrategy.*;
import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*;
/**
* Test for ChangeNumberIndexer class. All dependencies to the changelog DB
* interfaces are mocked. The ChangeNumberIndexer class simulates what the RS
* does to compute a changeNumber. The tests setup various topologies with their
* replicas.
* <p>
* All tests are written with this layout:
* <ul>
* <li>Initial setup where RS is stopped. Data are set into the changelog state
* DB, the replica DBs and the change number index DB.</li>
* <li>Simulate RS startup by calling {@link #startCNIndexer(DN...)}. This will
* start the change number indexer thread that will start computing change
* numbers and inserting them in the change number index db.</li>
* <li>Send events to the change number indexer thread by publishing update
* messages, sending heartbeat messages or replica offline messages.</li>
* </ul>
*/
@SuppressWarnings("javadoc")
public class ChangeNumberIndexerTest extends DirectoryServerTestCase
{
private static final class ReplicatedUpdateMsg extends UpdateMsg
{
private final DN baseDN;
private final boolean emptyCursor;
public ReplicatedUpdateMsg(DN baseDN, CSN csn)
{
this(baseDN, csn, false);
}
public ReplicatedUpdateMsg(DN baseDN, CSN csn, boolean emptyCursor)
{
super(csn, null);
this.baseDN = baseDN;
this.emptyCursor = emptyCursor;
}
public DN getBaseDN()
{
return baseDN;
}
public boolean isEmptyCursor()
{
return emptyCursor;
}
@Override
public String toString()
{
return "UpdateMsg("
+ "\"" + baseDN + " " + getCSN().getServerId() + "\""
+ ", csn=" + getCSN().toStringUI()
+ ")";
}
}
private static DN BASE_DN1;
private static DN BASE_DN2;
private static DN ADMIN_DATA_DN;
private static final int serverId1 = 101;
private static final int serverId2 = 102;
private static final int serverId3 = 103;
@Mock
private ChangelogDB changelogDB;
@Mock
private ChangeNumberIndexDB cnIndexDB;
@Mock
private ReplicationDomainDB domainDB;
private List<DN> eclEnabledDomains;
private MultiDomainDBCursor multiDomainCursor;
private Map<ReplicaId, SequentialDBCursor> replicaDBCursors;
private Map<DN, DomainDBCursor> domainDBCursors;
private ChangelogState initialState;
private Map<DN, ServerState> domainNewestCSNs;
private ECLEnabledDomainPredicate predicate;
private ChangeNumberIndexer cnIndexer;
@BeforeClass
public static void classSetup() throws Exception
{
TestCaseUtils.startServer();
BASE_DN1 = DN.valueOf("dc=example,dc=com");
BASE_DN2 = DN.valueOf("dc=world,dc=company");
ADMIN_DATA_DN = DN.valueOf("cn=admin data");
}
@BeforeMethod
public void setup() throws Exception
{
MockitoAnnotations.initMocks(this);
CursorOptions options = new CursorOptions(LESS_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY, null);
multiDomainCursor = new MultiDomainDBCursor(domainDB, options);
initialState = new ChangelogState();
replicaDBCursors = new HashMap<>();
domainDBCursors = new HashMap<>();
domainNewestCSNs = new HashMap<>();
when(changelogDB.getChangeNumberIndexDB()).thenReturn(cnIndexDB);
when(changelogDB.getReplicationDomainDB()).thenReturn(domainDB);
when(domainDB.getCursorFrom(any(MultiDomainServerState.class), eq(options))).thenReturn(multiDomainCursor);
}
@AfterMethod
public void tearDown() throws Exception
{
stopCNIndexer();
}
private static final String NO_DS = "noDS";
@Test
public void noDS() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
startCNIndexer();
assertExternalChangelogContent();
}
@Test(dependsOnMethods = { NO_DS })
public void oneDS() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
assertExternalChangelogContent(msg1);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSs() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
startCNIndexer();
assertExternalChangelogContent();
// simulate messages received out of order
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg2);
// do not start publishing to the changelog until we hear from serverId1
assertExternalChangelogContent();
publishUpdateMsg(msg1);
assertExternalChangelogContent(msg1);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsDifferentDomains() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1, BASE_DN2);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN2, serverId2);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN2, serverId2, 2);
publishUpdateMsg(msg1, msg2);
assertExternalChangelogContent(msg1);
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg3);
assertExternalChangelogContent(msg1, msg2);
}
/**
* This test tries to reproduce a very subtle implementation bug where:
* <ol>
* <li>the change number indexer has no more records to proceed, because all
* cursors are exhausted, so it calls wait()<li>
* <li>a new change Upd1 comes in for an exhausted cursor,
* medium consistency cannot move<li>
* <li>a new change Upd2 comes in for a cursor that is not already opened,
* medium consistency can move, so wake up the change number indexer<li>
* <li>on wake up, the change number indexer calls next(),
* advancing the CompositeDBCursor, which recycles the exhausted cursor,
* then calls next() on it, making it lose its change.
* CompositeDBCursor currentRecord == Upd1.<li>
* <li>on the next iteration of the loop in run(), a new cursor is created,
* triggering the creation of a new CompositeDBCursor => Upd1 is lost.
* CompositeDBCursor currentRecord == Upd2.<li>
* </ol>
*/
@Test(dependsOnMethods = { NO_DS })
public void twoDSsDoesNotLoseChanges() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
assertExternalChangelogContent(msg1);
addReplica(BASE_DN1, serverId2);
sendHeartbeat(BASE_DN1, serverId2, 2);
assertExternalChangelogContent(msg1);
// publish change that will not trigger a wake up of change number indexer,
// but will make it open a cursor on next wake up
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg2);
assertExternalChangelogContent(msg1);
// wake up change number indexer
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg3);
assertExternalChangelogContent(msg1, msg2);
sendHeartbeat(BASE_DN1, serverId2, 4);
// assert no changes have been lost
assertExternalChangelogContent(msg1, msg2, msg3);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneSendsNoUpdatesForSomeTime() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1Sid2 = msg(BASE_DN1, serverId2, 1);
final ReplicatedUpdateMsg emptySid2 = emptyCursor(BASE_DN1, serverId2);
final ReplicatedUpdateMsg msg2Sid1 = msg(BASE_DN1, serverId1, 2);
final ReplicatedUpdateMsg msg3Sid2 = msg(BASE_DN1, serverId2, 3);
// simulate no messages received during some time for replica 2
publishUpdateMsg(msg1Sid2, emptySid2, emptySid2, emptySid2, msg3Sid2, msg2Sid1);
assertExternalChangelogContent(msg1Sid2, msg2Sid1);
}
@Test(dependsOnMethods = { NO_DS })
public void threeDSsOneIsNotECLEnabledDomain() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(ADMIN_DATA_DN, serverId1);
addReplica(BASE_DN1, serverId2);
addReplica(BASE_DN1, serverId3);
startCNIndexer();
assertExternalChangelogContent();
// cn=admin data will does not participate in the external changelog
// so it cannot add to it
final ReplicatedUpdateMsg msg1 = msg(ADMIN_DATA_DN, serverId1, 1);
publishUpdateMsg(msg1);
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId3, 3);
publishUpdateMsg(msg2, msg3);
assertExternalChangelogContent(msg2);
}
@Test(dependsOnMethods = { NO_DS })
public void oneInitialDSAnotherDSJoining() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
assertExternalChangelogContent(msg1);
addReplica(BASE_DN1, serverId2);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg2);
assertExternalChangelogContent(msg1);
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg3);
assertExternalChangelogContent(msg1, msg2);
}
@Test(dependsOnMethods = { NO_DS })
public void oneInitialDSAnotherDSJoining2() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
addReplica(BASE_DN1, serverId2);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg2);
assertExternalChangelogContent(msg1);
sendHeartbeat(BASE_DN1, serverId1, 3);
assertExternalChangelogContent(msg1, msg2);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneSendingHeartbeats() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg1, msg2);
assertExternalChangelogContent(msg1);
sendHeartbeat(BASE_DN1, serverId1, 3);
assertExternalChangelogContent(msg1, msg2);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneGoingOffline() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg1, msg2);
assertExternalChangelogContent(msg1);
replicaOffline(BASE_DN1, serverId2, 3);
// MCP cannot move forward since no new updates from serverId1
assertExternalChangelogContent(msg1);
final ReplicatedUpdateMsg msg4 = msg(BASE_DN1, serverId1, 4);
publishUpdateMsg(msg4);
// MCP moves forward after receiving update from serverId1
// (last replica in the domain)
assertExternalChangelogContent(msg1, msg2, msg4);
// serverId2 comes online again
final ReplicatedUpdateMsg msg5 = msg(BASE_DN1, serverId2, 5);
publishUpdateMsg(msg5);
// MCP does not move until it knows what happens to serverId1
assertExternalChangelogContent(msg1, msg2, msg4);
sendHeartbeat(BASE_DN1, serverId1, 6);
// MCP moves forward
assertExternalChangelogContent(msg1, msg2, msg4, msg5);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneInitiallyOffline() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
initialState.addOfflineReplica(BASE_DN1, new CSN(1, 1, serverId1));
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId2, 2);
publishUpdateMsg(msg2);
// MCP does not wait for temporarily offline serverId1
assertExternalChangelogContent(msg2);
// serverId1 is back online, wait for changes from serverId2
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg3);
assertExternalChangelogContent(msg2);
final ReplicatedUpdateMsg msg4 = msg(BASE_DN1, serverId2, 4);
publishUpdateMsg(msg4);
// MCP moves forward
assertExternalChangelogContent(msg2, msg3);
}
/**
* Scenario:
* <ol>
* <li>Replica 1 publishes one change</li>
* <li>Replica 1 sends offline message</li>
* <li>RS stops</li>
* <li>RS starts</li>
* </ol>
*/
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneInitiallyWithChangesThenOffline() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
initialState.addOfflineReplica(BASE_DN1, new CSN(2, 1, serverId1));
startCNIndexer();
// blocked until we receive info for serverId2
assertExternalChangelogContent();
sendHeartbeat(BASE_DN1, serverId2, 3);
// MCP moves forward
assertExternalChangelogContent(msg1);
// do not wait for temporarily offline serverId1
final ReplicatedUpdateMsg msg4 = msg(BASE_DN1, serverId2, 4);
publishUpdateMsg(msg4);
assertExternalChangelogContent(msg1, msg4);
// serverId1 is back online, wait for changes from serverId2
final ReplicatedUpdateMsg msg5 = msg(BASE_DN1, serverId1, 5);
publishUpdateMsg(msg5);
assertExternalChangelogContent(msg1, msg4);
final ReplicatedUpdateMsg msg6 = msg(BASE_DN1, serverId2, 6);
publishUpdateMsg(msg6);
// MCP moves forward
assertExternalChangelogContent(msg1, msg4, msg5);
}
/**
* Scenario:
* <ol>
* <li>Replica 1 sends offline message</li>
* <li>Replica 1 starts</li>
* <li>Replica 1 publishes one change</li>
* <li>Replica 1 publishes a second change</li>
* <li>RS stops</li>
* <li>RS starts</li>
* </ol>
*/
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneInitiallyPersistedOfflineThenChanges() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
initialState.addOfflineReplica(BASE_DN1, new CSN(1, 1, serverId1));
final ReplicatedUpdateMsg msg2 = msg(BASE_DN1, serverId1, 2);
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg2, msg3);
startCNIndexer();
assertExternalChangelogContent();
// MCP moves forward because serverId1 is not really offline
// since we received a message from it newer than the offline replica msg
final ReplicatedUpdateMsg msg4 = msg(BASE_DN1, serverId2, 4);
publishUpdateMsg(msg4);
assertExternalChangelogContent(msg2, msg3);
// back to normal operations
sendHeartbeat(BASE_DN1, serverId1, 5);
assertExternalChangelogContent(msg2, msg3, msg4);
}
@Test(dependsOnMethods = { NO_DS })
public void twoDSsOneKilled() throws Exception
{
eclEnabledDomains = Arrays.asList(BASE_DN1);
addReplica(BASE_DN1, serverId1);
addReplica(BASE_DN1, serverId2);
startCNIndexer();
assertExternalChangelogContent();
final ReplicatedUpdateMsg msg1 = msg(BASE_DN1, serverId1, 1);
publishUpdateMsg(msg1);
// MCP cannot move forward: no news yet from serverId2
assertExternalChangelogContent();
sendHeartbeat(BASE_DN1, serverId2, 2);
// MCP moves forward: we know what serverId2 is at
assertExternalChangelogContent(msg1);
final ReplicatedUpdateMsg msg3 = msg(BASE_DN1, serverId1, 3);
publishUpdateMsg(msg3);
// MCP cannot move forward: serverId2 is the oldest CSN
assertExternalChangelogContent(msg1);
}
private void addReplica(DN baseDN, int serverId) throws Exception
{
final SequentialDBCursor replicaDBCursor = new SequentialDBCursor();
replicaDBCursors.put(ReplicaId.of(baseDN, serverId), replicaDBCursor);
if (predicate.isECLEnabledDomain(baseDN))
{
CursorOptions options = new CursorOptions(LESS_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY, null);
DomainDBCursor domainDBCursor = domainDBCursors.get(baseDN);
if (domainDBCursor == null)
{
domainDBCursor = new DomainDBCursor(baseDN, domainDB, options);
domainDBCursors.put(baseDN, domainDBCursor);
multiDomainCursor.addDomain(baseDN, null);
when(domainDB.getCursorFrom(eq(baseDN), any(ServerState.class), eq(options))).thenReturn(domainDBCursor);
}
domainDBCursor.addReplicaDB(serverId, null);
when(domainDB.getCursorFrom(eq(baseDN), eq(serverId), any(CSN.class), eq(options))).thenReturn(replicaDBCursor);
}
when(domainDB.getDomainNewestCSNs(baseDN)).thenReturn(getDomainNewestCSNs(baseDN));
initialState.addServerIdToDomain(serverId, baseDN);
}
private ServerState getDomainNewestCSNs(final DN baseDN)
{
ServerState serverState = domainNewestCSNs.get(baseDN);
if (serverState == null)
{
serverState = new ServerState();
domainNewestCSNs.put(baseDN, serverState);
}
return serverState;
}
private void startCNIndexer()
{
predicate = new ECLEnabledDomainPredicate()
{
@Override
public boolean isECLEnabledDomain(DN baseDN)
{
return eclEnabledDomains.contains(baseDN);
}
};
ChangelogStateProvider changeLogState = mock(ChangelogStateProvider.class);
when(changeLogState.getChangelogState()).thenReturn(initialState);
cnIndexer = new ChangeNumberIndexer(changelogDB, changeLogState, predicate)
{
/** {@inheritDoc} */
@Override
protected void notifyEntryAddedToChangelog(DN baseDN, long changeNumber,
MultiDomainServerState previousCookie, UpdateMsg msg) throws ChangelogException
{
// avoid problems with ChangelogBackend initialization
}
};
cnIndexer.start();
waitForWaitingState(cnIndexer);
}
private void stopCNIndexer() throws Exception
{
if (cnIndexer != null)
{
cnIndexer.initiateShutdown();
cnIndexer.join();
cnIndexer = null;
}
}
private ReplicatedUpdateMsg msg(DN baseDN, int serverId, long time)
{
return new ReplicatedUpdateMsg(baseDN, new CSN(time, 0, serverId));
}
private ReplicatedUpdateMsg emptyCursor(DN baseDN, int serverId)
{
return new ReplicatedUpdateMsg(baseDN, new CSN(0, 0, serverId), true);
}
private void publishUpdateMsg(ReplicatedUpdateMsg... msgs) throws Exception
{
for (ReplicatedUpdateMsg msg : msgs)
{
final SequentialDBCursor cursor =
replicaDBCursors.get(ReplicaId.of(msg.getBaseDN(), msg.getCSN().getServerId()));
if (msg.isEmptyCursor())
{
cursor.add(null);
}
else
{
cursor.add(msg);
}
}
for (ReplicatedUpdateMsg msg : msgs)
{
if (!msg.isEmptyCursor())
{
if (cnIndexer != null)
{
// indexer is running
cnIndexer.publishUpdateMsg(msg.getBaseDN(), msg);
}
else
{
// we are only setting up initial state, update the domain newest CSNs
getDomainNewestCSNs(msg.getBaseDN()).update(msg.getCSN());
}
}
}
waitForWaitingState(cnIndexer);
}
private void sendHeartbeat(DN baseDN, int serverId, int time) throws Exception
{
cnIndexer.publishHeartbeat(baseDN, new CSN(time, 0, serverId));
waitForWaitingState(cnIndexer);
}
private void replicaOffline(DN baseDN, int serverId, int time) throws Exception
{
cnIndexer.replicaOffline(baseDN, new CSN(time, 0, serverId));
waitForWaitingState(cnIndexer);
}
private void waitForWaitingState(final Thread t)
{
if (t == null)
{ // not started yet, do not wait
return;
}
State state = t.getState();
while (!state.equals(State.WAITING)
&& !state.equals(State.TIMED_WAITING)
&& !state.equals(State.TERMINATED))
{
Thread.yield();
state = t.getState();
}
assertThat(state).isIn(State.WAITING, State.TIMED_WAITING);
}
/**
* Asserts which records have been added to the CNIndexDB since starting the
* {@link ChangeNumberIndexer} thread.
*/
private void assertExternalChangelogContent(ReplicatedUpdateMsg... expectedMsgs)
throws Exception
{
final ArgumentCaptor<ChangeNumberIndexRecord> arg =
ArgumentCaptor.forClass(ChangeNumberIndexRecord.class);
verify(cnIndexDB, atLeast(0)).addRecord(arg.capture());
final List<ChangeNumberIndexRecord> allValues = arg.getAllValues();
// check it was not called more than expected
String desc1 = "actual was:<" + allValues + ">, but expected was:<" + Arrays.toString(expectedMsgs) + ">";
assertThat(allValues).as(desc1).hasSize(expectedMsgs.length);
for (int i = 0; i < expectedMsgs.length; i++)
{
final ReplicatedUpdateMsg expectedMsg = expectedMsgs[i];
final ChangeNumberIndexRecord record = allValues.get(i);
// check content in order
String desc2 = "actual was:<" + record + ">, but expected was:<" + expectedMsg + ">";
assertThat(record.getBaseDN()).as(desc2).isEqualTo(expectedMsg.getBaseDN());
assertThat(record.getCSN()).as(desc2).isEqualTo(expectedMsg.getCSN());
}
}
@DataProvider
public Object[][] precedingCSNDataProvider()
{
final int serverId = 42;
final int t = 1000;
return new Object[][] {
// @formatter:off
{ null, null, },
{ new CSN(t, 1, serverId), new CSN(t, 0, serverId), },
{ new CSN(t, 0, serverId), new CSN(t - 1, Integer.MAX_VALUE, serverId), },
// @formatter:on
};
}
}