/*
|
* 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.
|
* Portions Copyright 2026 3A Systems, LLC.
|
*/
|
package org.opends.server.replication.plugin;
|
|
import static org.mockito.Matchers.any;
|
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
import static org.testng.Assert.*;
|
|
import java.util.Arrays;
|
|
import org.forgerock.opendj.ldap.DN;
|
import org.mockito.ArgumentCaptor;
|
import org.opends.server.DirectoryServerTestCase;
|
import org.opends.server.replication.common.CSN;
|
import org.opends.server.replication.common.CSNGenerator;
|
import org.opends.server.replication.common.ServerState;
|
import org.opends.server.replication.protocol.LDAPUpdateMsg;
|
import org.opends.server.replication.protocol.ReplicaOfflineMsg;
|
import org.opends.server.replication.protocol.UpdateMsg;
|
import org.opends.server.replication.service.DSRSShutdownSync;
|
import org.opends.server.replication.service.ReplicationDomain;
|
import org.opends.server.types.operation.PluginOperation;
|
import org.testng.annotations.BeforeClass;
|
import org.testng.annotations.Test;
|
|
/**
|
* Tests the bookkeeping a replica does on its own changes: they are published in the order of
|
* their CSNs, and the announcement that the replica goes offline is made before the message is
|
* published, since the shutdown of a collocated replication server waits for that message to be
|
* forwarded - and stands only for a message the broker reports as written.
|
* <p>
|
* These tests need no server: the changes are built by a CSNGenerator, which needs nothing but
|
* a server id.
|
*/
|
@SuppressWarnings("javadoc")
|
@Test(groups = { "precommit", "replication" }, sequential = true)
|
public class PendingChangesTest extends DirectoryServerTestCase
|
{
|
private static final int SERVER_ID = 42;
|
/** A peer replication server the collocated one relays the message to. */
|
private static final int RS_ID = 11;
|
|
private static DN baseDN;
|
|
@BeforeClass
|
public static void classSetup() throws Exception
|
{
|
baseDN = DN.valueOf("dc=example,dc=com");
|
}
|
|
@Test
|
public void replicaOfflineMsgTheBrokerPublishedIsReportedAsSent() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
|
final CSN offlineCSN = pendingChanges.putReplicaOfflineMsg();
|
|
assertNotNull(offlineCSN, "the message was published and must be reported as sent");
|
final UpdateMsg published = onlyMsgPublishedBy(domain);
|
assertTrue(published instanceof ReplicaOfflineMsg, "published " + published);
|
assertEquals(published.getCSN(), offlineCSN);
|
}
|
|
/**
|
* The collocated replication server forwards the message as soon as it is on the wire, so an
|
* announcement made after the publish is one the forward found nothing to clear: nothing will
|
* ever remove it, and the shutdown waits out its whole grace period for a message which the
|
* topology already has.
|
* <p>
|
* The forward is reported from inside publish(), which is where the message reaches the
|
* session, so the race is reproduced rather than waited for.
|
*/
|
@Test
|
public void theReplicaOfflineMsgIsAnnouncedBeforeItIsPublished() throws Exception
|
{
|
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
|
final ReplicationDomain domain = mock(ReplicationDomain.class);
|
forwardWhilePublishing(domain, shutdownSync);
|
final PendingChanges pendingChanges = newPendingChanges(domain, shutdownSync);
|
|
pendingChanges.putReplicaOfflineMsg();
|
|
assertTrue(shutdownSync.canShutdown(baseDN),
|
"the message was forwarded, so nothing must hold the shutdown back any longer");
|
}
|
|
/**
|
* The announcement of a message the broker took stands until a peer forwards it: the
|
* withdrawal is for the message the broker refused, and an announcement taken back after a
|
* publish which succeeded would leave the shutdown nothing to wait for.
|
*/
|
@Test
|
public void theAnnouncementOfAPublishedMessageStandsUntilItIsForwarded() throws Exception
|
{
|
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
|
final PendingChanges pendingChanges =
|
newPendingChanges(domainWhichPublishes(true), shutdownSync);
|
|
pendingChanges.putReplicaOfflineMsg();
|
|
assertFalse(shutdownSync.canShutdown(baseDN),
|
"the message went out and nobody has forwarded it yet, so the shutdown must wait for it");
|
}
|
|
/**
|
* The broker writes nothing when it has no usable session, when the changes which come before
|
* this one still have to be republished by the recovery, or when it is stopped in between - and
|
* what was not written must not be reported as sent: the shutdown of a collocated replication
|
* server waits out the whole grace period of a message it was told about and which never
|
* reached the wire.
|
*/
|
@Test
|
public void replicaOfflineMsgTheBrokerRefusedIsNotReportedAsSent() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(false);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
|
assertNull(pendingChanges.putReplicaOfflineMsg(), "the broker refused the message");
|
|
assertTrue(onlyMsgPublishedBy(domain) instanceof ReplicaOfflineMsg, "it was attempted");
|
}
|
|
/**
|
* The announcement is made before the message is published, and the broker may refuse it
|
* once it is: an announcement which stayed would be one nobody will ever forward, and the
|
* shutdown would wait out its whole grace period for a message which never left. It is
|
* therefore withdrawn - and it is a withdrawal, not an announcement which was never made: the
|
* shutdown is held back while the broker holds the message.
|
*/
|
@Test
|
public void theAnnouncementOfAReplicaOfflineMsgTheBrokerRefusedIsWithdrawn() throws Exception
|
{
|
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
|
final ReplicationDomain domain = mock(ReplicationDomain.class);
|
refuseWhilePublishing(domain, shutdownSync);
|
final PendingChanges pendingChanges = newPendingChanges(domain, shutdownSync);
|
|
assertNull(pendingChanges.putReplicaOfflineMsg(), "the broker refused the message");
|
|
assertTrue(shutdownSync.canShutdown(baseDN),
|
"the message never reached the wire, so nothing must hold the shutdown back");
|
}
|
|
/**
|
* The message carries the newest CSN of the replica, so a change which is still in flight
|
* holds it back, and the broker is never even asked to publish it.
|
*/
|
@Test
|
public void replicaOfflineMsgQueuedBehindAnUncommittedChangeIsNotReportedAsSent() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
pendingChanges.putLocalOperation(newLocalOperation());
|
|
assertNull(pendingChanges.putReplicaOfflineMsg(), "nothing was published");
|
|
verify(domain, never()).publish(any(UpdateMsg.class));
|
}
|
|
/**
|
* A message which could not be published is given up on rather than left queued: the replica
|
* which could not announce itself offline is either shutting down, and the message dies with
|
* the process, or it is being disabled for an import or a configuration change - and once it
|
* comes back, announcing it offline on the session which follows would be a lie.
|
*/
|
@Test
|
public void replicaOfflineMsgWhichCouldNotBeSentIsNotPublishedLater() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
final CSN changeCSN = pendingChanges.putLocalOperation(newLocalOperation());
|
assertNull(pendingChanges.putReplicaOfflineMsg(), "nothing was published");
|
|
// The change which held the message back completes.
|
pendingChanges.commitAndPushCommittedChanges(changeCSN, mock(LDAPUpdateMsg.class));
|
|
final UpdateMsg published = onlyMsgPublishedBy(domain);
|
assertTrue(published instanceof LDAPUpdateMsg, "published " + published);
|
}
|
|
/**
|
* The announcement follows the publication rather than the queueing, so a message which a
|
* change in flight holds back is not announced: neither while it waits, nor when the change
|
* which held it back completes and the message is given up on. Announcing it either time
|
* would leave the shutdown waiting out its whole grace period for a forward which cannot
|
* happen.
|
*/
|
@Test
|
public void theReplicaOfflineMsgHeldBackByAChangeInFlightIsNeverAnnounced() throws Exception
|
{
|
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain, shutdownSync);
|
final CSN inFlight = pendingChanges.putLocalOperation(newLocalOperation());
|
|
pendingChanges.putReplicaOfflineMsg();
|
|
assertTrue(shutdownSync.canShutdown(baseDN),
|
"the message is still queued behind a change in flight, and nothing was announced");
|
|
pendingChanges.commitAndPushCommittedChanges(inFlight, mock(LDAPUpdateMsg.class));
|
|
assertTrue(shutdownSync.canShutdown(baseDN),
|
"the message was given up on with the change which held it back, and never announced");
|
}
|
|
/**
|
* A change the broker refused leaves the pending changes all the same: the replica has done
|
* it, its ServerState says so, and it is by finding that state ahead of the one its
|
* replication server reports that the next session republishes the change from the historical
|
* information of its entry. Only the offline announcement, which is stored nowhere, needs the
|
* answer of the broker.
|
*/
|
@Test
|
public void changeTheBrokerRefusedStillLeavesThePendingChanges() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(false);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
final CSN changeCSN = pendingChanges.putLocalOperation(newLocalOperation());
|
assertEquals(pendingChanges.size(), 1);
|
|
pendingChanges.commitAndPushCommittedChanges(changeCSN, mock(LDAPUpdateMsg.class));
|
|
assertTrue(onlyMsgPublishedBy(domain) instanceof LDAPUpdateMsg, "the change was published");
|
assertEquals(pendingChanges.size(), 0, "and is not queued for a second attempt");
|
}
|
|
/**
|
* The domain keeps the messages of its last local changes, in order, so that a recovery publishes
|
* them as they were done rather than rebuild them from the historical information, which holds
|
* nothing of a change a later one replaced (issue #1246).
|
*/
|
@Test
|
public void theMessagesOfTheLastLocalChangesAreKeptInOrder() throws Exception
|
{
|
final PendingChanges pendingChanges = newPendingChanges(domainWhichPublishes(true));
|
final LDAPUpdateMsg first = pushLocalChange(pendingChanges);
|
final LDAPUpdateMsg second = pushLocalChange(pendingChanges);
|
final LDAPUpdateMsg third = pushLocalChange(pendingChanges);
|
|
assertEquals(pendingChanges.recentChangesAfter(first.getCSN(), third.getCSN()).messages,
|
Arrays.asList(second, third));
|
assertEquals(pendingChanges.recentChangesAfter(first.getCSN(), second.getCSN()).messages,
|
Arrays.asList(second));
|
}
|
|
/**
|
* The messages kept go back to the CSN right before the first one: a recovery from there - from the
|
* change before it, done in the same millisecond - gets the first change as it was done, rather than
|
* rebuilt from the historical information.
|
*/
|
@Test
|
public void theMessagesKeptGoBackToRightBeforeTheFirstOne() throws Exception
|
{
|
final PendingChanges pendingChanges = newPendingChanges(domainWhichPublishes(true));
|
final LDAPUpdateMsg first = pushLocalChange(pendingChanges);
|
|
assertEquals(pendingChanges.recentChangesAfter(rightBefore(first), first.getCSN()).messages,
|
Arrays.asList(first));
|
}
|
|
/**
|
* The messages kept of a first change numbered 0 go back to the greatest sequence number of the
|
* millisecond before, which no CSN of the server comes after before that change: a recovery from there
|
* gets the change as it was done. A CSN which went back to the same millisecond would come after the
|
* change, and the recovery would rebuild it from the historical information instead.
|
*/
|
@Test
|
public void theMessagesKeptOfAChangeNumberedZeroGoBackToTheMillisecondBefore() throws Exception
|
{
|
final ServerState state = new ServerState();
|
// The generator goes on from the greatest sequence number, so its first change wraps to 0.
|
state.update(new CSN(0, Integer.MAX_VALUE, SERVER_ID));
|
final PendingChanges pendingChanges = new PendingChanges(new CSNGenerator(SERVER_ID, state),
|
domainWhichPublishes(true), new ShutdownSyncAnnouncer(new DSRSShutdownSync(), baseDN));
|
final LDAPUpdateMsg first = pushLocalChange(pendingChanges);
|
assertEquals(first.getCSN().getSeqnum(), 0);
|
|
final CSN justBefore = new CSN(first.getCSN().getTime() - 1, Integer.MAX_VALUE, SERVER_ID);
|
assertEquals(pendingChanges.recentChangesAfter(new CSN(0, 0, SERVER_ID), first.getCSN()).keptAfter,
|
justBefore);
|
assertEquals(pendingChanges.recentChangesAfter(justBefore, first.getCSN()).messages,
|
Arrays.asList(first));
|
}
|
|
/**
|
* Past as many changes as it keeps at most, the domain drops the oldest messages, and tells where
|
* the ones it kept begin: a recovery rebuilds the changes before that from the historical
|
* information.
|
*/
|
@Test
|
public void pastTheMostChangesKeptTheOldestAreDropped() throws Exception
|
{
|
final PendingChanges pendingChanges = newPendingChanges(domainWhichPublishes(true));
|
pendingChanges.setMaxRecentChanges(2);
|
final LDAPUpdateMsg first = pushLocalChange(pendingChanges);
|
final LDAPUpdateMsg second = pushLocalChange(pendingChanges);
|
final LDAPUpdateMsg third = pushLocalChange(pendingChanges);
|
|
assertEquals(pendingChanges.recentChangesAfter(first.getCSN(), third.getCSN()).messages,
|
Arrays.asList(second, third));
|
final PendingChanges.RecentChanges fromBefore =
|
pendingChanges.recentChangesAfter(new CSN(0, 0, SERVER_ID), third.getCSN());
|
assertNull(fromBefore.messages, "the messages kept do not go back that far");
|
assertEquals(fromBefore.keptAfter, first.getCSN());
|
}
|
|
/** Past as much of the heap as their messages take at most, the domain drops the oldest messages. */
|
@Test
|
public void pastTheMostBytesKeptTheOldestAreDropped() throws Exception
|
{
|
assertTrue(PendingChanges.MAX_RECENT_BYTES <= Runtime.getRuntime().maxMemory() / 16,
|
"a domain must not reserve more than a sixteenth of the heap for recent messages");
|
final PendingChanges pendingChanges = newPendingChanges(domainWhichPublishes(true));
|
final int half = sizeTaking(PendingChanges.MAX_RECENT_BYTES / 2);
|
final LDAPUpdateMsg first = pushLocalChange(pendingChanges, half);
|
final LDAPUpdateMsg second = pushLocalChange(pendingChanges, half);
|
assertEquals(pendingChanges.recentChangesAfter(rightBefore(first), second.getCSN()).messages,
|
Arrays.asList(first, second));
|
|
final LDAPUpdateMsg third = pushLocalChange(pendingChanges, 0);
|
|
assertEquals(pendingChanges.recentChangesAfter(first.getCSN(), third.getCSN()).messages,
|
Arrays.asList(second, third));
|
assertEquals(pendingChanges.recentChangesAfter(rightBefore(first), third.getCSN()).keptAfter, first.getCSN());
|
}
|
|
/**
|
* A change pushed again - the publish of the push before threw, and left it pending - is counted
|
* once: counted twice, it took more of the heap than it does, and was dropped though it was the only
|
* message kept.
|
*/
|
@Test
|
public void aChangePushedAgainIsCountedOnce() throws Exception
|
{
|
final ReplicationDomain domain = mock(ReplicationDomain.class);
|
when(domain.publish(any(UpdateMsg.class))).thenThrow(new IllegalStateException("the publish failed"))
|
.thenReturn(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
final CSN csn = pendingChanges.putLocalOperation(newLocalOperation());
|
final LDAPUpdateMsg large = newMsg(csn, sizeTaking(PendingChanges.MAX_RECENT_BYTES * 3 / 5));
|
try
|
{
|
pendingChanges.commitAndPushCommittedChanges(csn, large);
|
fail("the publish threw");
|
}
|
catch (IllegalStateException expected)
|
{
|
// The change is still pending, and the next push pushes it again.
|
}
|
|
final LDAPUpdateMsg next = pushLocalChange(pendingChanges, 0);
|
|
assertEquals(pendingChanges.recentChangesAfter(rightBefore(large), next.getCSN()).messages,
|
Arrays.asList(large, next));
|
}
|
|
/**
|
* A change the pending changes leave to a recovery is kept as well: the recovery of a later
|
* session, whose replication server misses it, publishes it as it was done.
|
*/
|
@Test
|
public void aChangeLeftToARecoveryIsKeptAsWell() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
when(domain.getServerState()).thenReturn(new ServerState());
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
final LDAPUpdateMsg published = pushLocalChange(pendingChanges);
|
pendingChanges.setRecovering(true);
|
|
final LDAPUpdateMsg kept = pushLocalChange(pendingChanges);
|
|
assertEquals(onlyMsgPublishedBy(domain), published);
|
assertEquals(pendingChanges.recentChangesAfter(published.getCSN(), kept.getCSN()).messages,
|
Arrays.asList(kept));
|
}
|
|
/**
|
* The messages kept are forgotten once the domain reads its state from data which may lack their
|
* changes, and none of the changes before is taken for kept: the state read back can be older than
|
* changes done since the domain started. The messages of the changes pushed from there are kept.
|
*/
|
@Test
|
public void theMessagesKeptAreForgotten() throws Exception
|
{
|
final ReplicationDomain domain = domainWhichPublishes(true);
|
final PendingChanges pendingChanges = newPendingChanges(domain);
|
final CSN first = pushLocalChange(pendingChanges).getCSN();
|
final CSN last = pushLocalChange(pendingChanges).getCSN();
|
// The state read back is older than the last change.
|
when(domain.getLastLocalChange()).thenReturn(first);
|
|
pendingChanges.forgetRecentChanges();
|
|
final PendingChanges.RecentChanges fromFirst = pendingChanges.recentChangesAfter(first, last);
|
assertNull(fromFirst.messages, "the messages were forgotten");
|
assertNull(fromFirst.keptAfter, "and the change after the last one of the state is not taken for kept");
|
|
final LDAPUpdateMsg next = pushLocalChange(pendingChanges);
|
final LDAPUpdateMsg afterNext = pushLocalChange(pendingChanges);
|
assertNull(pendingChanges.recentChangesAfter(first, afterNext.getCSN()).messages,
|
"the change forgotten is not taken for kept");
|
assertEquals(pendingChanges.recentChangesAfter(next.getCSN(), afterNext.getCSN()).messages,
|
Arrays.asList(afterNext));
|
}
|
|
@Test
|
public void aCommittedChangeHeldBehindAnotherIsNotKeptAfterTheDataWasReplaced() throws Exception
|
{
|
final PendingChanges pending = newPendingChanges(domainWhichPublishes(false));
|
final CSN first = pending.putLocalOperation(newLocalOperation());
|
final CSN second = pending.putLocalOperation(newLocalOperation());
|
final LDAPUpdateMsg secondMsg = newMsg(second, 0);
|
pending.commitAndPushCommittedChanges(second, secondMsg);
|
|
pending.forgetRecentChanges();
|
pending.commitAndPushCommittedChanges(first, newMsg(first, 0));
|
final LDAPUpdateMsg after = pushLocalChange(pending);
|
|
assertNull(pending.recentChangesAfter(first, after.getCSN()).messages,
|
"the change made on the replaced data must not be treated as kept");
|
assertEquals(pending.recentChangesAfter(rightBefore(after), after.getCSN()).messages, Arrays.asList(after));
|
}
|
|
@Test
|
public void anUncommittedChangeAtDataReloadIsNotKeptAfterItCommits() throws Exception
|
{
|
final PendingChanges pending = newPendingChanges(domainWhichPublishes(false));
|
final CSN old = pending.putLocalOperation(newLocalOperation());
|
pending.forgetRecentChanges();
|
pending.commitAndPushCommittedChanges(old, newMsg(old, 0));
|
final LDAPUpdateMsg after = pushLocalChange(pending);
|
|
assertNull(pending.recentChangesAfter(rightBefore(newMsg(old, 0)), after.getCSN()).messages,
|
"a change pending at reload must not be published from the recent messages");
|
assertEquals(pending.recentChangesAfter(rightBefore(after), after.getCSN()).messages, Arrays.asList(after));
|
}
|
|
/** Pushes a local change whose message takes no bytes, and returns its message. */
|
private LDAPUpdateMsg pushLocalChange(PendingChanges pendingChanges)
|
{
|
return pushLocalChange(pendingChanges, 0);
|
}
|
|
/** Pushes a local change whose message has the provided size, and returns its message. */
|
private LDAPUpdateMsg pushLocalChange(PendingChanges pendingChanges, int size)
|
{
|
final CSN csn = pendingChanges.putLocalOperation(newLocalOperation());
|
final LDAPUpdateMsg msg = newMsg(csn, size);
|
pendingChanges.commitAndPushCommittedChanges(csn, msg);
|
return msg;
|
}
|
|
private static LDAPUpdateMsg newMsg(CSN csn, int size)
|
{
|
final LDAPUpdateMsg msg = mock(LDAPUpdateMsg.class);
|
when(msg.getCSN()).thenReturn(csn);
|
when(msg.size()).thenReturn(size);
|
return msg;
|
}
|
|
/** Returns the size of a message which takes the provided part of the heap once kept. */
|
private static int sizeTaking(long footprint)
|
{
|
return (int) ((footprint - PendingChanges.MESSAGE_OVERHEAD) / 2);
|
}
|
|
/** Returns the CSN right before the one of the message: the change before it, in the same millisecond. */
|
private static CSN rightBefore(LDAPUpdateMsg msg)
|
{
|
return new CSN(msg.getCSN().getTime(), msg.getCSN().getSeqnum() - 1, SERVER_ID);
|
}
|
|
/**
|
* Reports the forward of the message from within the publish which puts it on the wire, after
|
* checking from there that the announcement is already in place, and publishes it. The forward
|
* is the one of a peer the message was never recorded as queued for, which is what a forward
|
* racing the announcement looks like.
|
*/
|
private void forwardWhilePublishing(
|
final ReplicationDomain domain, final DSRSShutdownSync shutdownSync)
|
{
|
doAnswer(invocation -> {
|
final UpdateMsg msg = (UpdateMsg) invocation.getArguments()[0];
|
if (msg instanceof ReplicaOfflineMsg)
|
{
|
assertFalse(shutdownSync.canShutdown(baseDN),
|
"the message must be announced before it is published");
|
shutdownSync.replicaOfflineMsgForwarded(baseDN, msg.getCSN(), RS_ID);
|
}
|
return true;
|
}).when(domain).publish(any(UpdateMsg.class));
|
}
|
|
/**
|
* Refuses to publish the message, the way a broker with no usable session does, after checking
|
* from within the publish that the announcement is already in place.
|
*/
|
private void refuseWhilePublishing(
|
final ReplicationDomain domain, final DSRSShutdownSync shutdownSync)
|
{
|
doAnswer(invocation -> {
|
assertFalse(shutdownSync.canShutdown(baseDN),
|
"the message must be announced before it is published");
|
return false;
|
}).when(domain).publish(any(UpdateMsg.class));
|
}
|
|
private PendingChanges newPendingChanges(ReplicationDomain domain)
|
{
|
return newPendingChanges(domain, new DSRSShutdownSync());
|
}
|
|
/**
|
* The pending changes of a replica whose domain announces itself through the shutdown sync,
|
* with the announcer LDAPReplicationDomain hands its own pending changes.
|
*/
|
private PendingChanges newPendingChanges(
|
final ReplicationDomain domain, final DSRSShutdownSync shutdownSync)
|
{
|
return new PendingChanges(new CSNGenerator(SERVER_ID, 0), domain,
|
new ShutdownSyncAnnouncer(shutdownSync, baseDN));
|
}
|
|
/** A domain whose broker accepts, or refuses, whatever it is given to publish. */
|
private ReplicationDomain domainWhichPublishes(boolean accepted)
|
{
|
final ReplicationDomain domain = mock(ReplicationDomain.class);
|
when(domain.publish(any(UpdateMsg.class))).thenReturn(accepted);
|
return domain;
|
}
|
|
/** A local operation, i.e. one this replica must publish to the other replicas. */
|
private PluginOperation newLocalOperation()
|
{
|
final PluginOperation operation = mock(PluginOperation.class);
|
when(operation.isSynchronizationOperation()).thenReturn(false);
|
return operation;
|
}
|
|
/**
|
* The single message the domain was asked to publish, failing the test if it published
|
* anything else: a message which is not sent and a message which is sent twice are both the
|
* kind of mistake these tests are about.
|
*/
|
private UpdateMsg onlyMsgPublishedBy(ReplicationDomain domain)
|
{
|
final ArgumentCaptor<UpdateMsg> published = ArgumentCaptor.forClass(UpdateMsg.class);
|
verify(domain).publish(published.capture());
|
return published.getValue();
|
}
|
}
|