/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.replication.server;
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.ldap.ModificationType.REPLACE;
import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING;
import static org.opends.server.util.CollectionUtils.newArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.forgerock.opendj.ldap.DN;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.ModifyMsg;
import org.opends.server.replication.protocol.ReplicaOfflineMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.changelog.api.ChangelogException;
import org.opends.server.replication.server.changelog.api.DBCursor;
import org.opends.server.types.Attributes;
import org.opends.server.types.Modification;
import org.opends.server.util.TimeThread;
import org.testng.annotations.Test;
/**
* A handler which is behind reads the changes for its consumer from the changelog, and goes back
* to its in-memory queue once the queue holds what the consumer lacks - see issue #1206. The queue
* is ordered by CSN and drops its oldest changes when it is full, so holding the oldest change the
* consumer lacks does not mean holding all of them. A change which reaches the queue late with an
* older CSN - relayed by another replication server, or published again by a recovery - can be
* the oldest change the consumer lacks while the queue lacks newer changes of another replica: the
* ones it dropped before that change came, or the ones which reached the domain before the handler
* did. Served from the queue then, the consumer would be given the changes of that replica which
* the queue still holds, and its state would move past the ones in between for good.
*
* Each test drives a handler which no domain feeds, from a single thread, as the writer of a
* session does. A change "arrives" the way {@code ReplicationServerDomain.put()} hands it over to
* a registered handler: written to the changelog, then given to the queue. A change written to the
* changelog only stands for one which reached the domain before the handler was registered.
* Replica {@link #A} stands for a directory server connected to the replication server, replica
* {@link #B} for one whose changes another replication server relays late: the changes of B carry
* older CSNs than the changes of A which arrive with them.
*/
@SuppressWarnings("javadoc")
public class MessageHandlerCatchUpTest extends ReplicationTestCase
{
private static final int RS_ID = 120;
private static final int A = 121;
private static final int B = 122;
private static final int C = 123;
private static final int WINDOW_SIZE = 100;
/** A queue no test fills. */
private static final int LARGE_QUEUE_SIZE = 100;
/** How long the handler is given to come up with a change it has: it reads them from memory. */
private static final long NEXT_MESSAGE_TIMEOUT_MS = 5000;
/**
* The case of the description of #1206, on a handler connected for long: the queue, full, has
* dropped a change of A the consumer lacks, and a change of B with an older CSN has come in
* after, into the room the drop left. The change of B is the oldest change the consumer lacks,
* and it is in the queue - but the change of A is not, and must still be sent.
*/
@Test
public void aChangeTheFullQueueDroppedIsSentWhenAnOlderOneCameInAfterIt() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerDroppedDb");
final DroppingQueue queue = new DroppingQueue(baseDN, TimeThread.getTime() - 60000, A, true);
final MessageHandler handler = newHandler(replicationServer, baseDN, queue.size, queue.consumerState);
queue.arriveIn(replicationServer, handler);
assertThat(csnsOf(nextMessages(handler, queue.changesTheConsumerLacks.size())))
.as("the consumer was not sent every change it lacks, in order")
.containsExactlyElementsOf(csnsOf(queue.changesTheConsumerLacks));
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The case of the reconnections of #1206: a new handler, whose queue does not hold the changes
* of A which reached the domain before it did. A change of B with an older CSN then arrives with
* a change of A: the change of B is the oldest change the consumer lacks and it is in the queue,
* but the earlier changes of A are not, and must still be sent.
*/
@Test
public void aChangeWhichReachedTheDomainBeforeTheHandlerIsSentWhenAnOlderOneIsQueued() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerBeforeDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 15, 1, B));
final List earlierChangesOfA = changesOfA(baseDN, t, 1, 3);
final DeleteMsg a4 = newDeleteMsg(baseDN, "a4", new CSN(t + 24, 4, A));
// the consumer has the changes of B up to b0, and none of A
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, stateOf(b0));
publishToChangelogOnly(replicationServer, baseDN, b0);
publishToChangelogOnly(replicationServer, baseDN, earlierChangesOfA);
arrive(replicationServer, handler, a4);
arrive(replicationServer, handler, b1);
final List expected = new ArrayList<>();
expected.add(b1);
expected.addAll(earlierChangesOfA);
expected.add(a4);
assertThat(csnsOf(nextMessages(handler, expected.size())))
.as("the consumer was not sent every change it lacks, in order")
.containsExactlyElementsOf(csnsOf(expected));
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The same as {@link #aChangeWhichReachedTheDomainBeforeTheHandlerIsSentWhenAnOlderOneIsQueued()}
* for a replica which writes nothing more once the handler is there: the queue is handed changes
* of B only, never one of A, and the earlier changes of A must still be sent.
*/
@Test
public void aChangeOfAReplicaTheQueueIsNeverHandedIsSentWhenAnOlderOneIsQueued() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerQuietDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 15, 1, B));
final List earlierChangesOfA = changesOfA(baseDN, t, 1, 3);
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, stateOf(b0));
publishToChangelogOnly(replicationServer, baseDN, earlierChangesOfA);
arrive(replicationServer, handler, b0);
arrive(replicationServer, handler, b1);
final List expected = new ArrayList<>();
expected.add(b1);
expected.addAll(earlierChangesOfA);
assertThat(csnsOf(nextMessages(handler, expected.size())))
.as("the consumer was not sent every change it lacks, in order")
.containsExactlyElementsOf(csnsOf(expected));
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* A change of B with an older CSN arrives while the handler reads the changelog, too late for the
* read: the first change read is a change of A, and it is in the queue - but the change of B
* before it is not one the consumer has, and must still be sent.
*/
@Test
public void aChangeWhichArrivedWhileTheChangelogWasReadIsSent() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
ChangeAfterTheRead domain = null;
try
{
replicationServer = newReplicationServer("messageHandlerWhileReadDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final DeleteMsg a0 = newDeleteMsg(baseDN, "a0", new CSN(t + 10, 0, A));
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 15, 1, B));
final List changesOfA = changesOfA(baseDN, t, 1, 2);
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, stateOf(b0, a0));
domain = new ChangeAfterTheRead(replicationServer, handler, b1);
arrive(replicationServer, handler, b0);
arrive(replicationServer, handler, a0);
for (UpdateMsg msg : changesOfA)
{
arrive(replicationServer, handler, msg);
}
final List expected = new ArrayList<>(changesOfA);
expected.add(b1);
assertThat(csnsOf(nextMessages(handler, expected.size())))
.as("the consumer was not sent every change it lacks")
.containsExactlyElementsOf(csnsOf(expected));
}
finally
{
deregister(domain);
removeQuietly(replicationServer);
}
}
/**
* The same as {@link #aChangeWhichArrivedWhileTheChangelogWasReadIsSent()} for a handler whose
* consumer has everything the changelog held: the read gives nothing, and the change of B, which
* arrives after it, is the oldest change of the full queue, which drops it at once. The queue
* holds nothing the consumer lacks, but the change of B must still be sent.
*/
@Test
public void aChangeTheFullQueueDroppedWhileTheChangelogWasReadIsSent() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
ChangeAfterTheRead domain = null;
try
{
replicationServer = newReplicationServer("messageHandlerDroppedWhileReadDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 15, 1, B));
final List changesOfA = changesOfA(baseDN, t, 1, 5);
final ServerState consumerState = stateOf(b0, changesOfA.get(changesOfA.size() - 1));
// full with the changes of A: one more change makes it drop its oldest one
final MessageHandler handler = newHandler(replicationServer, baseDN, changesOfA.size(), consumerState);
domain = new ChangeAfterTheRead(replicationServer, handler, b1);
for (UpdateMsg msg : changesOfA)
{
arrive(replicationServer, handler, msg);
}
assertThat(csnsOf(nextMessages(handler, 1)))
.as("the consumer was not sent the change the queue dropped")
.containsExactly(b1.getCSN());
}
finally
{
deregister(domain);
removeQuietly(replicationServer);
}
}
/**
* The same queue as {@link #aChangeTheFullQueueDroppedIsSentWhenAnOlderOneCameInAfterIt()}, for
* a consumer which already has the change of A the queue dropped: the queue holds everything the
* consumer lacks, and the handler serves it from the queue again, as soon as it finds there the
* oldest change the consumer lacks.
*/
@Test
public void aHandlerWhoseQueueHoldsWhatTheConsumerLacksFollowsTheQueueAgain() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerFollowsDb");
final DroppingQueue queue = new DroppingQueue(baseDN, TimeThread.getTime() - 60000, A, true);
final ServerState consumerState = queue.consumerState.duplicate();
consumerState.update(queue.dropped.getCSN());
final MessageHandler handler = newHandler(replicationServer, baseDN, queue.size, consumerState);
queue.arriveIn(replicationServer, handler);
final List expected = new ArrayList<>(queue.changesTheConsumerLacks);
expected.remove(queue.dropped);
assertServedFromTheQueueAfterTheFirst(handler, expected);
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The queue dropped a change the changelog no longer holds, as after the changelog was cleared
* for a new generation id, of a replica which writes nothing more: nothing can send it to the
* consumer, and it must not keep the handler from its queue, which holds everything else the
* consumer lacks.
*/
@Test
public void aDroppedChangeTheChangelogNoLongerHoldsDoesNotHoldTheHandlerBack() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerNoLongerHeldDb");
final DroppingQueue queue = new DroppingQueue(baseDN, TimeThread.getTime() - 60000, C, false);
final MessageHandler handler = newHandler(replicationServer, baseDN, queue.size, queue.consumerState);
queue.arriveIn(replicationServer, handler);
final List expected = new ArrayList<>(queue.changesTheConsumerLacks);
expected.remove(queue.dropped);
assertServedFromTheQueueAfterTheFirst(handler, expected);
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* A replica offline message carries the CSN of no change, and no state of a consumer ever holds
* it: one in the queue before the first change the consumer lacks must not keep the handler from
* its queue, even once its replica, back online, has written a change the consumer lacks.
*/
@Test
public void aReplicaOfflineMessageDoesNotHoldTheHandlerBack() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerOfflineDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg c0 = newDeleteMsg(baseDN, "c0", new CSN(t, 0, C));
final DeleteMsg a1 = newDeleteMsg(baseDN, "a1", new CSN(t + 1, 1, A));
final DeleteMsg a2 = newDeleteMsg(baseDN, "a2", new CSN(t + 3, 2, A));
final DeleteMsg c3 = newDeleteMsg(baseDN, "c3", new CSN(t + 4, 3, C));
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, stateOf(c0, a1));
arrive(replicationServer, handler, c0);
arrive(replicationServer, handler, a1);
// a replica offline message is not written to the changelog, its CSN goes to the state of
// the replica instead: ReplicationServerDomain.publishUpdateMsg()
handler.add(new ReplicaOfflineMsg(new CSN(t + 2, 0, C)));
arrive(replicationServer, handler, a2);
arrive(replicationServer, handler, c3);
final List expected = new ArrayList<>();
expected.add(a2);
expected.add(c3);
assertServedFromTheQueueAfterTheFirst(handler, expected);
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The queue still holds a change the changelog no longer holds, as after the changelog was
* cleared for a new generation id, and the changelog gives the handler nothing but the replica
* offline message of a replica which wrote since: no read of the changelog can give the consumer
* that change, and the handler must not read the changelog again and again for it. It goes back
* to its queue, leaving out what the queue holds before the offline message, that change too.
*/
@Test
public void aQueuedChangeTheChangelogNoLongerHoldsDoesNotHoldTheHandlerBack() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerQueuedNoLongerHeldDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg c0 = newDeleteMsg(baseDN, "c0", new CSN(t, 0, C));
final DeleteMsg a1 = newDeleteMsg(baseDN, "a1", new CSN(t + 1, 1, A));
final CSN offlineC = new CSN(t + 2, 0, C);
final DeleteMsg a2 = newDeleteMsg(baseDN, "a2", new CSN(t + 3, 2, A));
// the consumer has every change the changelog holds
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, stateOf(c0));
publishToChangelogOnly(replicationServer, baseDN, c0);
// the change of a changelog cleared since
handler.add(a1);
// every read of the changelog gives it, as no state of a consumer ever holds its CSN
replicationServer.getChangelogDB().getReplicationDomainDB().notifyReplicaOffline(baseDN, offlineC);
assertThat(csnsOf(nextMessages(handler, 1)))
.as("the handler kept reading the changelog for a change no read can give")
.containsExactly(offlineC);
assertThat(handler.isFollowing()).isTrue();
arrive(replicationServer, handler, a2);
final List sent = nextMessages(handler, 1);
assertThat(csnsOf(sent)).containsExactly(a2.getCSN());
assertThat(sent.get(0).fromTheChangelog).isFalse();
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The full queue of a handler drops a replica offline message, which the domain hands to the
* handler of a peer replication server: the consumer has every change of that replica the
* changelog holds, and no state of a consumer ever holds the CSN of the message, so it must not
* keep the handler from its queue, which holds every change the consumer lacks.
*/
@Test
public void aDroppedReplicaOfflineMessageDoesNotHoldTheHandlerBack() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer("messageHandlerDroppedOfflineDb");
final long t = TimeThread.getTime() - 60000;
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final List changesOfA = changesOfA(baseDN, t, 1, 5);
// full with the changes of A: they make it drop the offline message, its oldest one
final MessageHandler handler = newHandler(replicationServer, baseDN, changesOfA.size(), stateOf(b0));
publishToChangelogOnly(replicationServer, baseDN, b0);
handler.add(new ReplicaOfflineMsg(new CSN(t + 5, 0, B)));
for (UpdateMsg msg : changesOfA)
{
arrive(replicationServer, handler, msg);
}
assertServedFromTheQueueAfterTheFirst(handler, changesOfA);
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* A handler which has been handed a change catches up from the changelog until a read gives
* nothing, and goes back to its queue: the change handed to it after that read is served from
* the queue.
*/
@Test
public void aHandlerWhichCaughtUpFromTheChangelogFollowsTheQueueAgain() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
ChangeAfterTheRead domain = null;
try
{
replicationServer = newReplicationServer("messageHandlerCaughtUpDb");
final long t = TimeThread.getTime() - 60000;
final List changesOfA = changesOfA(baseDN, t, 1, 3);
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, new ServerState());
domain = new ChangeAfterTheRead(replicationServer, handler, changesOfA.get(2), true);
publishToChangelogOnly(replicationServer, baseDN, changesOfA.get(0));
arrive(replicationServer, handler, changesOfA.get(1));
assertServedFromTheQueueAfterAReadGaveNothing(handler, changesOfA);
}
finally
{
deregister(domain);
removeQuietly(replicationServer);
}
}
/**
* The same as {@link #aHandlerWhichCaughtUpFromTheChangelogFollowsTheQueueAgain()} for a handler
* which is never handed the changes of a replica, as the handler of a peer replication server is
* never handed the changes of the replicas connected to other replication servers: the domain held
* a change of B when it handed the queue its first change, so the handler reads it from the
* changelog, although it finds in its queue the first change it reads, then goes back to its queue.
*/
@Test
public void aHandlerWhichReadTheChangesOfAReplicaItIsNeverHandedFollowsTheQueueAgain() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
ReplicationServer replicationServer = null;
ChangeAfterTheRead domain = null;
try
{
replicationServer = newReplicationServer("messageHandlerNeverHandedDb");
final long t = TimeThread.getTime() - 60000;
final List changesOfA = changesOfA(baseDN, t, 1, 2);
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 30, 1, B));
final MessageHandler handler = newHandler(replicationServer, baseDN, LARGE_QUEUE_SIZE, new ServerState());
domain = new ChangeAfterTheRead(replicationServer, handler, changesOfA.get(1), true);
publishToChangelogOnly(replicationServer, baseDN, b1);
arrive(replicationServer, handler, changesOfA.get(0));
final List expected = new ArrayList<>();
expected.add(changesOfA.get(0));
expected.add(b1);
expected.add(changesOfA.get(1));
assertServedFromTheQueueAfterAReadGaveNothing(handler, expected);
}
finally
{
deregister(domain);
removeQuietly(replicationServer);
}
}
/**
* A queue of {@link #size} changes which drops a change the consumer lacks, then takes a change
* of B with an older CSN in the room the drop left - which only a queue bounded by its size in
* bytes leaves, as it drops whole changes: one large change, dropped, leaves room for a small one.
*
* The queue is not full until it holds 5 changes. Changes {@code b0} and {@code a0}, which the
* consumer has, show that the handler has been handed changes for long; the large change pushes
* the queue over its size with the fifth change, so the queue drops {@code b0}, then {@code a0},
* then the large change. The changes after it are small, so {@code b1} comes into the room the
* large change left: the queue ends up holding {@code b1} and {@code a2} .. {@code a6}.
*/
private final class DroppingQueue
{
private final List arrivals = new ArrayList<>();
/** The changes the consumer lacks, in the order it must be sent them. */
private final List changesTheConsumerLacks = new ArrayList<>();
private final ServerState consumerState;
private final UpdateMsg dropped;
private final boolean droppedInTheChangelog;
private final int size;
/**
* @param droppedReplica
* the replica of the change the queue drops
* @param droppedInTheChangelog
* whether the changelog holds the change the queue drops
*/
private DroppingQueue(DN baseDN, long t, int droppedReplica, boolean droppedInTheChangelog)
throws Exception
{
final DeleteMsg b0 = newDeleteMsg(baseDN, "b0", new CSN(t, 0, B));
final DeleteMsg a0 = newDeleteMsg(baseDN, "a0", new CSN(t + 10, 0, A));
final DeleteMsg b1 = newDeleteMsg(baseDN, "b1", new CSN(t + 15, 1, B));
// a replace of a large value: the size of a delete does not count its DN
dropped = new ModifyMsg(new CSN(t + 21, 1, droppedReplica), DN.valueOf("cn=large," + baseDN),
newArrayList(new Modification(REPLACE, Attributes.create("description", "a1".repeat(1000)))),
"uuid");
this.droppedInTheChangelog = droppedInTheChangelog;
final List keptChangesOfA = changesOfA(baseDN, t, 2, 6);
arrivals.add(b0);
arrivals.add(a0);
arrivals.add(dropped);
arrivals.addAll(keptChangesOfA);
arrivals.add(b1);
changesTheConsumerLacks.add(b1);
changesTheConsumerLacks.add(dropped);
changesTheConsumerLacks.addAll(keptChangesOfA);
consumerState = stateOf(b0, a0);
// room for b1 and a2 .. a6 in bytes, as many changes as the queue ever holds, and not for the
// large change
int keptBytes = b1.size();
for (UpdateMsg msg : keptChangesOfA)
{
keptBytes += msg.size();
}
size = Math.max(arrivals.size(), (keptBytes + 99) / 100);
assertThat(dropped.size())
.as("the large change must push the queue over its size in bytes on its own")
.isGreaterThan(size * 100);
}
private void arriveIn(ReplicationServer replicationServer, MessageHandler handler) throws Exception
{
for (UpdateMsg msg : arrivals)
{
if (msg != dropped || droppedInTheChangelog)
{
arrive(replicationServer, handler, msg);
}
else
{
handler.add(msg);
}
}
}
}
/**
* The domain of a handler, which hands it a change once it has read the changelog for the first
* time - or once a read of the changelog gave nothing - before it looks at its queue: the domain
* writes the changes it receives and hands them over on threads of their own, and nothing keeps
* one from coming in between. The change comes the way {@code ReplicationServerDomain.put()}
* hands it over - see {@link #arrive}.
*/
private final class ChangeAfterTheRead extends ReplicationServerDomain
{
private final ReplicationServer replicationServer;
private final MessageHandler handler;
private final boolean onceAReadGivesNothing;
private UpdateMsg change;
private ChangeAfterTheRead(ReplicationServer replicationServer, MessageHandler handler, UpdateMsg change)
{
this(replicationServer, handler, change, false);
}
/**
* @param onceAReadGivesNothing
* whether the change comes once a read of the changelog gave nothing, rather than once
* the changelog was read for the first time
*/
private ChangeAfterTheRead(ReplicationServer replicationServer, MessageHandler handler, UpdateMsg change,
boolean onceAReadGivesNothing)
{
super(handler.getBaseDN(), replicationServer);
this.replicationServer = replicationServer;
this.handler = handler;
this.change = change;
this.onceAReadGivesNothing = onceAReadGivesNothing;
handler.replicationServerDomain = this;
}
@Override
public String getMonitorInstanceName()
{
// apart from the one of the domain of the replication server, which it would replace
return super.getMonitorInstanceName() + " for " + MessageHandlerCatchUpTest.class.getSimpleName();
}
@Override
public DBCursor getCursorFrom(ServerState startAfterServerState) throws ChangelogException
{
final DBCursor read = super.getCursorFrom(startAfterServerState);
return new DBCursor()
{
private boolean gaveSomething;
@Override
public UpdateMsg getRecord()
{
return read.getRecord();
}
@Override
public boolean next() throws ChangelogException
{
final boolean hasNext = read.next();
gaveSomething |= hasNext;
return hasNext;
}
@Override
public void close()
{
read.close();
if (change != null && (!onceAReadGivesNothing || !gaveSomething))
{
final UpdateMsg msg = change;
change = null;
try
{
arrive(replicationServer, handler, msg);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
};
}
}
private static void deregister(ReplicationServerDomain domain)
{
if (domain != null)
{
DirectoryServer.deregisterMonitorProvider(domain);
}
}
/** A message the handler came up with, and whether it read it from the changelog. */
private static final class Sent
{
private final UpdateMsg msg;
private final boolean fromTheChangelog;
private Sent(UpdateMsg msg, boolean fromTheChangelog)
{
this.msg = msg;
this.fromTheChangelog = fromTheChangelog;
}
}
/**
* The handler sends the given changes, in order, and serves them from its queue from the second
* one on: it finds the first one in its queue and goes back to it.
*/
private void assertServedFromTheQueueAfterTheFirst(MessageHandler handler, List expected)
throws Exception
{
final List sent = nextMessages(handler, expected.size());
assertThat(csnsOf(sent)).containsExactlyElementsOf(csnsOf(expected));
assertThat(sent.get(1).fromTheChangelog)
.as("the handler did not go back to its queue, which holds every change the consumer lacks")
.isFalse();
assertThat(handler.isFollowing()).isTrue();
}
/**
* The handler sends the given changes, in order, and serves the last one from its queue: it is
* handed to the queue once a read of the changelog gave nothing, see {@link ChangeAfterTheRead}.
*/
private void assertServedFromTheQueueAfterAReadGaveNothing(MessageHandler handler, List expected)
throws Exception
{
final List sent = nextMessages(handler, expected.size());
assertThat(csnsOf(sent)).containsExactlyElementsOf(csnsOf(expected));
assertThat(sent.get(sent.size() - 1).fromTheChangelog)
.as("the handler did not go back to its queue once the changelog had nothing more for the consumer")
.isFalse();
assertThat(handler.isFollowing()).isTrue();
}
/**
* The next messages the handler comes up with for its consumer, at most {@code count}: fewer
* when it has none to come up with. It is asked from one thread, as the writer of a session asks.
*/
private List nextMessages(final MessageHandler handler, int count) throws Exception
{
final ExecutorService writer = Executors.newSingleThreadExecutor();
try
{
final List sent = new ArrayList<>();
for (int i = 0; i < count; i++)
{
final Future next = writer.submit(new Callable()
{
@Override
public Sent call() throws Exception
{
final UpdateMsg msg = handler.getNextMessage();
return msg != null ? new Sent(msg, handler.isLastMessageFromLateQueue()) : null;
}
});
final Sent nextSent;
try
{
nextSent = next.get(NEXT_MESSAGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
catch (TimeoutException e)
{
// it waits for a change to reach its queue: it has nothing more for the consumer
handler.setConsumerActive(false);
break;
}
if (nextSent == null)
{
break;
}
sent.add(nextSent);
}
return sent;
}
finally
{
writer.shutdownNow();
}
}
private MessageHandler newHandler(ReplicationServer replicationServer, DN baseDN, int queueSize,
ServerState consumerState) throws Exception
{
final MessageHandler handler = new MessageHandler(queueSize, replicationServer);
handler.setBaseDNAndDomain(baseDN, false);
handler.setInitialServerState(consumerState);
return handler;
}
/** Hands a change over the way {@code ReplicationServerDomain.put()} does. */
private void arrive(ReplicationServer replicationServer, MessageHandler handler, UpdateMsg msg)
throws Exception
{
publishToChangelogOnly(replicationServer, handler.getBaseDN(), msg);
handler.add(msg);
}
private void publishToChangelogOnly(ReplicationServer replicationServer, DN baseDN, List msgs)
throws Exception
{
for (UpdateMsg msg : msgs)
{
publishToChangelogOnly(replicationServer, baseDN, msg);
}
}
private void publishToChangelogOnly(ReplicationServer replicationServer, DN baseDN, UpdateMsg msg)
throws Exception
{
replicationServer.getChangelogDB().getReplicationDomainDB().publishUpdateMsg(baseDN, msg);
}
/** Changes {@code a} .. {@code a} of A, a second apart, the first 20 s after B's first one. */
private List changesOfA(DN baseDN, long t, int first, int last) throws Exception
{
final List changes = new ArrayList<>();
for (int i = first; i <= last; i++)
{
changes.add(newDeleteMsg(baseDN, "a" + i, new CSN(t + 20 + i, i, A)));
}
return changes;
}
private static ServerState stateOf(UpdateMsg... msgs)
{
final ServerState state = new ServerState();
for (UpdateMsg msg : msgs)
{
state.update(msg.getCSN());
}
return state;
}
private static List csnsOf(List> msgs)
{
final List csns = new ArrayList<>();
for (Object msg : msgs)
{
csns.add(msg instanceof Sent ? ((Sent) msg).msg.getCSN() : ((UpdateMsg) msg).getCSN());
}
return csns;
}
private DeleteMsg newDeleteMsg(DN baseDN, String rdnValue, CSN csn) throws Exception
{
return new DeleteMsg(DN.valueOf("cn=" + rdnValue + "," + baseDN), csn, "uuid");
}
private ReplicationServer newReplicationServer(String dbDirName) throws Exception
{
return new ReplicationServer(new ReplServerFakeConfiguration(
TestCaseUtils.findFreePort(), dbDirName, 0, RS_ID, 0, WINDOW_SIZE, new TreeSet()));
}
private void removeQuietly(ReplicationServer replicationServer)
{
try
{
if (replicationServer != null)
{
remove(replicationServer);
}
}
catch (Exception ignored)
{
// the test has already reported what matters
}
}
}