/*
* 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.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING;
import static org.opends.server.util.CollectionUtils.newArrayList;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
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.atomic.AtomicBoolean;
import org.forgerock.opendj.ldap.DN;
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ReplSessionSecurity;
import org.opends.server.replication.protocol.ReplicaOfflineMsg;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.opends.server.replication.protocol.Session;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.replication.service.DSRSShutdownSync;
import org.opends.server.replication.service.ReplicationBroker;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.TestTimer;
import org.testng.annotations.Test;
/**
* The shutdown of a replication server must let a ReplicaOfflineMsg sent by a collocated
* directory server be forwarded to the other replication servers of the topology before the
* server handlers are stopped - stopping them deactivates their consumer, clears their message
* queue and closes their session, after which the message can no longer be sent.
*
* Most tests drive {@link DSRSShutdownSync} directly rather than through a collocated directory
* server: the contract they pin is when the shutdown of the replication server waits, and how
* long. {@link #thePeerReceivesTheReplicaOfflineMsgBeforeTheShutdownReturns()} pins the outcome
* those waits exist for, on a peer connected through the real handshake.
*/
@SuppressWarnings("javadoc")
public class ReplicationServerShutdownSyncTest extends ReplicationTestCase
{
private static final int SOCKET_TIMEOUT_MS = 30000;
/** A session end nobody owns is discarded, so its cleanup waits far less than a live one. */
private static final int DISCARDED_SESSION_TIMEOUT_MS = 2000;
private static final int REMOTE_RS_ID = 92;
private static final int REMOTE_DS_ID = 93;
/** The collocated replica whose ReplicaOfflineMsg the shutdown waits for. */
private static final int LOCAL_DS_ID = 94;
/** Time given to the forwarding thread before it releases the shutdown. */
private static final long FORWARD_DELAY = 500;
/** How often the domains of {@link #theGracePeriodIsSharedByAllTheDomainsOfOneShutdown()}
* announce themselves offline again while the shutdown is waiting for them. */
private static final long REANNOUNCE_INTERVAL = 200;
@Test
public void shutdownWaitsForTheReplicaOfflineMsgToBeForwarded() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
try (ServerSocket listen = TestCaseUtils.bindFreePort())
{
listen.setSoTimeout(SOCKET_TIMEOUT_MS);
replicationServer = newReplicationServer(shutdownSync, "shutdownSyncWaitDb", 8221);
final Session[] sessionPair = connectSessionPair(listen, getReplSessionSecurity());
try (Session remoteEnd = sessionPair[0];
Session session = sessionPair[1])
{
registerConnectedReplicationServer(replicationServer, baseDN, session);
final long startTime = System.nanoTime();
shutdownSync.replicaOfflineMsgSent(baseDN, newOfflineCSN());
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isGreaterThanOrEqualTo(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
}
finally
{
removeQuietly(replicationServer);
}
}
@Test
public void shutdownResumesAsSoonAsTheReplicaOfflineMsgIsForwarded() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
Thread forwarder = null;
try (ServerSocket listen = TestCaseUtils.bindFreePort())
{
listen.setSoTimeout(SOCKET_TIMEOUT_MS);
replicationServer = newReplicationServer(shutdownSync, "shutdownSyncForwardDb", 8222);
final Session[] sessionPair = connectSessionPair(listen, getReplSessionSecurity());
try (Session remoteEnd = sessionPair[0];
Session session = sessionPair[1])
{
registerConnectedReplicationServer(replicationServer, baseDN, session);
final CSN offlineCSN = newOfflineCSN();
forwarder = newForwarderThread(shutdownSync, baseDN, offlineCSN);
shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN);
final long startTime = System.nanoTime();
forwarder.start();
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isGreaterThanOrEqualTo(FORWARD_DELAY)
.isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
}
finally
{
joinQuietly(forwarder);
removeQuietly(replicationServer);
}
}
/**
* The outcome the grace period exists for, end to end: a peer replication server connected
* through the real handshake has received the ReplicaOfflineMsg of the collocated replica by
* the time the shutdown returns.
*
* The waiting tests above measure durations only, so they stay green if the wait is moved
* after the handlers are stopped - which reintroduces OPENDJ-1453 and loses the message. This
* one fails in that case.
*/
@Test
public void thePeerReceivesTheReplicaOfflineMsgBeforeTheShutdownReturns() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
ReplicationBroker broker = null;
FakePeerReplicationServer peer = null;
Thread publisher = null;
try
{
final int replicationPort = TestCaseUtils.findFreePort();
replicationServer =
newReplicationServer(shutdownSync, "shutdownSyncDeliveryDb", 8226, replicationPort);
broker = openReplicationSession(baseDN, LOCAL_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID);
peer = new FakePeerReplicationServer(replicationPort, REMOTE_RS_ID, baseDN, EMPTY_DN_GENID);
final ReplicationServerDomain domain =
replicationServer.getReplicationServerDomain(baseDN, true);
waitForConnectedReplicationServer(domain);
final Future received = peer.receiveReplicaOfflineMsg();
/*
* The replica announces itself offline once the shutdown of the replication server is
* already waiting for the message, which is the ordering the grace period exists for.
*/
final CSN offlineCSN = newOfflineCSN();
shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN);
publisher = newPublisherThread(broker, offlineCSN);
final long startTime = System.nanoTime();
publisher.start();
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
final ReplicaOfflineMsg forwarded = received.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(forwarded)
.as("the peer replication server was never told that the replica went offline, its "
+ "read ended with: %s", peer.readerFailure())
.isNotNull();
assertThat(forwarded.getCSN().getServerId()).isEqualTo(LOCAL_DS_ID);
assertThat(elapsed).isGreaterThanOrEqualTo(FORWARD_DELAY)
.isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
finally
{
joinQuietly(publisher);
closeQuietly(peer);
stop(broker);
removeQuietly(replicationServer);
}
}
/**
* Only a peer replication server learning about the offline replica ends the wait.
* ReplicationServerDomain.put() never queues a ReplicaOfflineMsg for a directory server, but
* the changelog cursor of a directory server which is catching up synthesizes one from the
* offline CSN of the replica, so the writer serving a directory server can publish it - and
* the peer replication servers would still know nothing.
*/
@Test
public void theForwardToADirectoryServerDoesNotEndTheWait() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
ReplicationBroker broker = null;
try (ServerSocket listen = TestCaseUtils.bindFreePort())
{
listen.setSoTimeout(SOCKET_TIMEOUT_MS);
final int replicationPort = TestCaseUtils.findFreePort();
replicationServer =
newReplicationServer(shutdownSync, "shutdownSyncDataServerForwardDb", 8227, replicationPort);
broker = openReplicationSession(baseDN, REMOTE_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID);
final Session[] sessionPair = connectSessionPair(listen, getReplSessionSecurity());
try (Session remoteEnd = sessionPair[0];
Session session = sessionPair[1])
{
// a peer replication server, so that the shutdown does wait for the message: what this
// test pins is that the directory server receiving it is not what ends that wait
registerConnectedReplicationServer(replicationServer, baseDN, session);
final ReplicationServerDomain domain =
replicationServer.getReplicationServerDomain(baseDN, true);
final DataServerHandler dsHandler = waitForConnectedDirectoryServer(domain);
final CSN offlineCSN = newOfflineCSN();
final long startTime = System.nanoTime();
shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN);
// the very message the shutdown waits for, so only the guard of the writer can save it
dsHandler.add(new ReplicaOfflineMsg(offlineCSN));
// the directory server did receive it, so its writer went through the forwarding code
assertThat(waitForSpecificMsg(broker, ReplicaOfflineMsg.class).getCSN().getServerId())
.isEqualTo(LOCAL_DS_ID);
assertThat(elapsedMillis(startTime))
.as("the fixture must deliver the message well inside the grace period, otherwise "
+ "the wait asserted below cannot be told apart from a slow delivery")
.isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD / 2);
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed)
.as("the message published to a directory server ended the wait of the shutdown")
.isGreaterThanOrEqualTo(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
}
finally
{
stop(broker);
removeQuietly(replicationServer);
}
}
/**
* With no other replication server connected there is nobody to forward the message to, so
* waiting would only delay the shutdown of a standalone server by the whole grace period.
*/
@Test
public void shutdownIsNotDelayedWhenNoOtherReplicationServerCanForwardTheMessage() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
try
{
replicationServer = newReplicationServer(shutdownSync, "shutdownSyncAloneDb", 8223);
replicationServer.getReplicationServerDomain(baseDN, true);
final long startTime = System.nanoTime();
shutdownSync.replicaOfflineMsgSent(baseDN, newOfflineCSN());
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The writer serving a directory server must not hold back the shutdown either: it used to
* loop on the pending message until the grace period expired, although its handler had already
* been shut down - which deactivates its consumer and leaves the loop nothing to take.
*/
@Test
public void shutdownIsNotDelayedByTheWriterServingADirectoryServer() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
ReplicationBroker broker = null;
try
{
final int replicationPort = TestCaseUtils.findFreePort();
replicationServer =
newReplicationServer(shutdownSync, "shutdownSyncDataServerDb", 8225, replicationPort);
broker = openReplicationSession(baseDN, REMOTE_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID);
final long startTime = System.nanoTime();
shutdownSync.replicaOfflineMsgSent(baseDN, newOfflineCSN());
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
finally
{
stop(broker);
removeQuietly(replicationServer);
}
}
@Test
public void shutdownIsNotDelayedWhenNoReplicaOfflineMsgIsPending() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
ReplicationServer replicationServer = null;
try (ServerSocket listen = TestCaseUtils.bindFreePort())
{
listen.setSoTimeout(SOCKET_TIMEOUT_MS);
replicationServer = newReplicationServer(shutdownSync, "shutdownSyncNoMsgDb", 8224);
final Session[] sessionPair = connectSessionPair(listen, getReplSessionSecurity());
try (Session remoteEnd = sessionPair[0];
Session session = sessionPair[1])
{
registerConnectedReplicationServer(replicationServer, baseDN, session);
final long startTime = System.nanoTime();
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
}
finally
{
removeQuietly(replicationServer);
}
}
/**
* The domains of a replication server are shut down one after the other, so the grace period
* must bound the whole shutdown and not each of its domains: a process with several base DNs
* would otherwise pay it once per domain.
*
* Both domains keep announcing themselves offline while the shutdown is running, so neither
* wait can be ended by a forward and each of them runs to its bound - one grace period in
* total if it is shared, one per domain otherwise.
*/
@Test
public void theGracePeriodIsSharedByAllTheDomainsOfOneShutdown() throws Exception
{
final DN baseDN1 = DN.valueOf(TEST_ROOT_DN_STRING);
final DN baseDN2 = DN.valueOf("dc=world,dc=company");
final DSRSShutdownSync shutdownSync = new DSRSShutdownSync();
final AtomicBoolean stopped = new AtomicBoolean();
ReplicationServer replicationServer = null;
Thread reAnnouncer = null;
try (ServerSocket listen1 = TestCaseUtils.bindFreePort();
ServerSocket listen2 = TestCaseUtils.bindFreePort())
{
listen1.setSoTimeout(SOCKET_TIMEOUT_MS);
listen2.setSoTimeout(SOCKET_TIMEOUT_MS);
replicationServer = newReplicationServer(shutdownSync, "shutdownSyncSharedDeadlineDb", 8228);
final Session[] sessionPair1 = connectSessionPair(listen1, getReplSessionSecurity());
final Session[] sessionPair2 = connectSessionPair(listen2, getReplSessionSecurity());
try (Session remoteEnd1 = sessionPair1[0];
Session session1 = sessionPair1[1];
Session remoteEnd2 = sessionPair2[0];
Session session2 = sessionPair2[1])
{
registerConnectedReplicationServer(replicationServer, baseDN1, session1);
registerConnectedReplicationServer(replicationServer, baseDN2, session2);
/*
* Announce both domains offline here rather than leaving it to the thread below: the
* wait of the shutdown must be armed whatever that thread has had time to run.
*/
final CSNGenerator csns = new CSNGenerator(LOCAL_DS_ID, 0);
shutdownSync.replicaOfflineMsgSent(baseDN1, csns.newCSN());
shutdownSync.replicaOfflineMsgSent(baseDN2, csns.newCSN());
reAnnouncer = newReAnnouncerThread(shutdownSync, baseDN1, baseDN2, stopped);
reAnnouncer.start();
final long startTime = System.nanoTime();
replicationServer.shutdown();
final long elapsed = elapsedMillis(startTime);
assertThat(elapsed).isGreaterThanOrEqualTo(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
assertThat(elapsed)
.as("each domain waited its own grace period instead of sharing one deadline")
.isLessThan(2 * DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD);
}
}
finally
{
stopped.set(true);
joinQuietly(reAnnouncer);
removeQuietly(replicationServer);
}
}
private ReplicationServer newReplicationServer(DSRSShutdownSync shutdownSync, String dbDirName,
int serverId) throws Exception
{
return newReplicationServer(shutdownSync, dbDirName, serverId, TestCaseUtils.findFreePort());
}
private ReplicationServer newReplicationServer(DSRSShutdownSync shutdownSync, String dbDirName,
int serverId, int replicationPort) throws Exception
{
return new ReplicationServer(new ReplServerFakeConfiguration(
replicationPort, dbDirName, 0, serverId, 0, 100, new TreeSet()), shutdownSync);
}
/**
* Registers a peer replication server on the domain as the handshake does, but without the
* protocol exchange: the handler this leaves behind has no writer, which is enough for the
* tests which only need a domain with a connected peer.
*/
private void registerConnectedReplicationServer(
ReplicationServer replicationServer, DN baseDN, Session session) throws Exception
{
final ReplicationServerDomain domain = replicationServer.getReplicationServerDomain(baseDN, true);
final ReplicationServerHandler rsHandler =
new ReplicationServerHandler(session, 100, replicationServer, 100);
rsHandler.serverId = REMOTE_RS_ID;
rsHandler.serverURL = "127.0.0.1:1636";
rsHandler.setBaseDNAndDomain(baseDN, false);
domain.lock();
try
{
domain.register(rsHandler);
}
finally
{
domain.release();
}
}
private void waitForConnectedReplicationServer(final ReplicationServerDomain domain)
throws Exception
{
newConnectionTimer().repeatUntilSuccess(new TestTimer.CallableVoid()
{
@Override
public void call() throws Exception
{
assertThat(domain.getConnectedRSs())
.as("the peer replication server never connected").containsKey(REMOTE_RS_ID);
}
});
}
private DataServerHandler waitForConnectedDirectoryServer(final ReplicationServerDomain domain)
throws Exception
{
return newConnectionTimer().repeatUntilSuccess(new Callable()
{
@Override
public DataServerHandler call() throws Exception
{
final DataServerHandler dsHandler = domain.getConnectedDSs().get(REMOTE_DS_ID);
assertThat(dsHandler).as("the directory server never connected").isNotNull();
return dsHandler;
}
});
}
private static TestTimer newConnectionTimer()
{
return new TestTimer.Builder()
.maxSleep(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.sleepTimes(10, TimeUnit.MILLISECONDS)
.toTimer();
}
private Thread newForwarderThread(final DSRSShutdownSync shutdownSync, final DN baseDN,
final CSN offlineCSN)
{
return new Thread(new Runnable()
{
@Override
public void run()
{
if (!sleepQuietly(FORWARD_DELAY))
{
return;
}
shutdownSync.replicaOfflineMsgForwarded(baseDN, offlineCSN);
}
});
}
private Thread newPublisherThread(final ReplicationBroker broker, final CSN offlineCSN)
{
return new Thread(new Runnable()
{
@Override
public void run()
{
if (!sleepQuietly(FORWARD_DELAY))
{
return;
}
broker.publish(new ReplicaOfflineMsg(offlineCSN));
}
});
}
private Thread newReAnnouncerThread(final DSRSShutdownSync shutdownSync, final DN baseDN1,
final DN baseDN2, final AtomicBoolean stopped)
{
return new Thread(new Runnable()
{
@Override
public void run()
{
final CSNGenerator csns = new CSNGenerator(LOCAL_DS_ID, 0);
while (!stopped.get())
{
shutdownSync.replicaOfflineMsgSent(baseDN1, csns.newCSN());
shutdownSync.replicaOfflineMsgSent(baseDN2, csns.newCSN());
if (!sleepQuietly(REANNOUNCE_INTERVAL))
{
return;
}
}
}
});
}
/** Milliseconds elapsed since a {@link System#nanoTime()} reading, the clock the waits use. */
private static long elapsedMillis(long startTime)
{
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
}
/** The CSN of a message the collocated replica announces, as PendingChanges generates it. */
private static CSN newOfflineCSN()
{
return new CSNGenerator(LOCAL_DS_ID, 0).newCSN();
}
private static boolean sleepQuietly(long millis)
{
try
{
Thread.sleep(millis);
return true;
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
return false;
}
}
/** Teardown must never mask the primary assertion failure. */
private void removeQuietly(ReplicationServer replicationServer)
{
try
{
remove(replicationServer);
}
catch (Exception ignored)
{
}
}
private void joinQuietly(Thread thread)
{
if (thread != null)
{
try
{
thread.join(SOCKET_TIMEOUT_MS);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
}
}
private void closeQuietly(FakePeerReplicationServer peer)
{
if (peer != null)
{
peer.close();
}
}
/**
* Establishes a connected session pair over the given listen socket, as a remote server
* connecting to the RS would. The TLS negotiation performed by the session factories needs both
* ends handshaking at the same time, so the client end runs on its own thread.
*
* @return the two sessions: the remote (client) end first, then the local (server) end to hand
* to the handler under test
*/
private Session[] connectSessionPair(ServerSocket listenSocket, final ReplSessionSecurity security)
throws Exception
{
final Socket clientSocket = new Socket("127.0.0.1", listenSocket.getLocalPort());
clientSocket.setTcpNoDelay(true);
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future clientEnd = null;
Socket serverSocket = null;
Session serverEnd = null;
boolean connected = false;
try
{
clientEnd = executor.submit(new Callable()
{
@Override
public Session call() throws Exception
{
return security.createClientSession(clientSocket, SOCKET_TIMEOUT_MS);
}
});
serverSocket = listenSocket.accept();
serverSocket.setTcpNoDelay(true);
serverEnd = security.createServerSession(serverSocket, SOCKET_TIMEOUT_MS);
assertThat(serverEnd).as("could not create a session for the handler under test").isNotNull();
final Session[] sessionPair =
new Session[] { clientEnd.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS), serverEnd };
connected = true;
return sessionPair;
}
finally
{
if (!connected)
{
// Nobody owns either end yet: close whatever they managed to create.
closeClientEndQuietly(clientEnd, clientSocket);
closeServerEndQuietly(serverEnd, serverSocket);
}
executor.shutdown();
}
}
private void closeServerEndQuietly(Session serverEnd, Socket serverSocket)
{
if (serverEnd != null)
{
serverEnd.close();
}
else
{
StaticUtils.close(serverSocket);
}
}
private void closeClientEndQuietly(Future clientEnd, Socket clientSocket)
{
if (clientEnd != null)
{
try
{
final Session session = clientEnd.get(DISCARDED_SESSION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
if (session != null)
{
session.close();
}
}
catch (Exception ignored)
{
clientEnd.cancel(true);
}
}
StaticUtils.close(clientSocket);
}
/**
* A peer replication server which connects to the replication server under test and completes
* the handshake, so that the handler it leaves behind on the domain has a real writer and can
* actually forward what the domain pushes to it.
*/
private static final class FakePeerReplicationServer
{
private final Session session;
private final ExecutorService reader = Executors.newSingleThreadExecutor();
/** Why the peer stopped reading, so that a missing message can be told from a failed one. */
private volatile Exception readerFailure;
FakePeerReplicationServer(int replicationPort, int serverId, DN baseDN, long generationId)
throws Exception
{
final Socket socket = new Socket();
Session newSession = null;
boolean handshaken = false;
try
{
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress("127.0.0.1", replicationPort), SOCKET_TIMEOUT_MS);
newSession = getReplSessionSecurity().createClientSession(socket, SOCKET_TIMEOUT_MS);
final String serverURL = "127.0.0.1:" + socket.getLocalPort();
final byte groupId = (byte) 1;
newSession.publish(new ReplServerStartMsg(serverId, serverURL, baseDN, 100,
new ServerState(), generationId, false, groupId, 5000));
final ReplServerStartMsg inStartMsg =
waitForSpecificMsg(newSession, ReplServerStartMsg.class);
if (!inStartMsg.getSSLEncryption())
{
newSession.stopEncryption();
}
newSession.publish(new TopologyMsg(null,
newArrayList(new RSInfo(serverId, serverURL, generationId, groupId, 1))));
waitForSpecificMsg(newSession, TopologyMsg.class);
handshaken = true;
}
finally
{
if (!handshaken)
{
// The caller has no handle on this peer yet, so nothing else would close it.
reader.shutdownNow();
if (newSession != null)
{
newSession.close();
}
else
{
StaticUtils.close(socket);
}
}
}
session = newSession;
}
/** Returns the first ReplicaOfflineMsg this peer receives, or null if its session ends first. */
Future receiveReplicaOfflineMsg()
{
return reader.submit(new Callable()
{
@Override
public ReplicaOfflineMsg call()
{
try
{
while (true)
{
final ReplicationMsg msg = session.receive();
if (msg instanceof ReplicaOfflineMsg)
{
return (ReplicaOfflineMsg) msg;
}
}
}
catch (Exception e)
{
// The session is closed when the replication server completes its shutdown: whatever
// has not arrived by then never will.
readerFailure = e;
return null;
}
}
});
}
/** Returns what ended the read of this peer, null if nothing did. */
Exception readerFailure()
{
return readerFailure;
}
void close()
{
reader.shutdownNow();
session.close();
}
}
}