| | |
| | | import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING; |
| | | import static org.opends.server.util.CollectionUtils.newArrayList; |
| | | |
| | | import java.io.IOException; |
| | | import java.net.InetSocketAddress; |
| | | import java.net.ServerSocket; |
| | | import java.net.Socket; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.TreeSet; |
| | | import java.util.concurrent.Callable; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.CountDownLatch; |
| | | 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 java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | 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.CSNGenerator; |
| | | import org.opends.server.replication.common.RSInfo; |
| | | import org.opends.server.replication.common.ServerState; |
| | | import org.opends.server.replication.protocol.DeleteMsg; |
| | | 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.protocol.WindowMsg; |
| | | import org.opends.server.replication.service.DSRSShutdownSync; |
| | | import org.opends.server.replication.service.ReplicationBroker; |
| | | import org.opends.server.util.StaticUtils; |
| | |
| | | * <p> |
| | | * 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. |
| | | * long. {@link #thePeerReceivesTheReplicaOfflineMsgBeforeTheShutdownReturns()} and |
| | | * {@link #theShutdownWaitsForEveryPeerToBeToldTheReplicaWentOffline()} pin the outcome those |
| | | * waits exist for, on peers connected through the real handshake. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ReplicationServerShutdownSyncTest extends ReplicationTestCase |
| | |
| | | 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; |
| | | /** The peer replication server whose writer is held back by a full send window. */ |
| | | private static final int HELD_BACK_RS_ID = 95; |
| | | /** The peer replication server whose handshake is aborted while the message is pushed. */ |
| | | private static final int ABORTED_RS_ID = 96; |
| | | /** Send window a peer advertises when nothing has to hold its writer back. */ |
| | | private static final int PEER_WINDOW = 100; |
| | | /** |
| | | * Send window of the peer which is held back: one change fills it, and the message which |
| | | * follows stays with its writer until the peer gives it credit again. |
| | | */ |
| | | private static final int HELD_BACK_PEER_WINDOW = 1; |
| | | /** Time given to the forwarding thread before it releases the shutdown. */ |
| | | private static final long FORWARD_DELAY = 500; |
| | | /** |
| | | * Time given to a writer to reach the message it was handed once its send window is opened, |
| | | * well short of the grace period so that a writer which never gets there is reported as such |
| | | * rather than as a shutdown which waited. |
| | | */ |
| | | private static final long WRITER_REACTION_TIMEOUT_MS = 10000; |
| | | /** 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; |
| | |
| | | |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | waitForConnectedReplicationServer(domain); |
| | | final Future<ReplicaOfflineMsg> received = peer.receiveReplicaOfflineMsg(); |
| | | waitForConnectedReplicationServer(domain, REMOTE_RS_ID); |
| | | final Future<ReplicaOfflineMsg> received = peer.receive(ReplicaOfflineMsg.class); |
| | | |
| | | /* |
| | | * The replica announces itself offline once the shutdown of the replication server is |
| | |
| | | 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()) |
| | | + "read ended with: %s", peer.failure()) |
| | | .isNotNull(); |
| | | assertThat(forwarded.getCSN().getServerId()).isEqualTo(LOCAL_DS_ID); |
| | | assertThat(elapsed).isGreaterThanOrEqualTo(FORWARD_DELAY) |
| | |
| | | } |
| | | |
| | | /** |
| | | * The grace period covers every peer replication server, not only the fastest of them. The |
| | | * message is queued for each of them and published by its own writer, and the shutdown clears |
| | | * the queue and closes the session of whoever has not published it yet: a peer whose writer is |
| | | * held back would be left unaware that the replica went offline, and its change number indexer |
| | | * would keep the medium consistency point pinned to the last change of that replica. |
| | | */ |
| | | @Test |
| | | public void theShutdownWaitsForEveryPeerToBeToldTheReplicaWentOffline() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | final RecordingShutdownSync shutdownSync = new RecordingShutdownSync(); |
| | | ReplicationServer replicationServer = null; |
| | | ReplicationBroker broker = null; |
| | | FakePeerReplicationServer peer = null; |
| | | FakePeerReplicationServer heldBackPeer = null; |
| | | Thread windowOpener = null; |
| | | try |
| | | { |
| | | final int replicationPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = |
| | | newReplicationServer(shutdownSync, "shutdownSyncEveryPeerDb", 8229, replicationPort); |
| | | broker = |
| | | openReplicationSession(baseDN, LOCAL_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID); |
| | | peer = new FakePeerReplicationServer( |
| | | replicationPort, REMOTE_RS_ID, baseDN, EMPTY_DN_GENID, PEER_WINDOW); |
| | | heldBackPeer = new FakePeerReplicationServer( |
| | | replicationPort, HELD_BACK_RS_ID, baseDN, EMPTY_DN_GENID, HELD_BACK_PEER_WINDOW); |
| | | |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | waitForConnectedReplicationServer(domain, REMOTE_RS_ID); |
| | | waitForConnectedReplicationServer(domain, HELD_BACK_RS_ID); |
| | | |
| | | /* |
| | | * One change fills the send window of the held back peer: its writer publishes that one and |
| | | * then blocks on the permit of the next message, so the ReplicaOfflineMsg stays with it |
| | | * while its neighbour forwards the same message right away. |
| | | */ |
| | | final CSNGenerator csns = new CSNGenerator(LOCAL_DS_ID, 0); |
| | | final Future<DeleteMsg> windowFiller = heldBackPeer.receive(DeleteMsg.class); |
| | | broker.publish(new DeleteMsg(DN.valueOf("uid=offline," + TEST_ROOT_DN_STRING), |
| | | csns.newCSN(), "offline-entry-uuid")); |
| | | assertThat(windowFiller.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the send window of the held back peer was never filled, its exchange ended " |
| | | + "with: %s", heldBackPeer.failure()) |
| | | .isNotNull(); |
| | | |
| | | final Future<ReplicaOfflineMsg> received = peer.receive(ReplicaOfflineMsg.class); |
| | | final Future<ReplicaOfflineMsg> receivedWhenHeldBack = |
| | | heldBackPeer.receive(ReplicaOfflineMsg.class); |
| | | final CSN offlineCSN = csns.newCSN(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN); |
| | | broker.publish(new ReplicaOfflineMsg(offlineCSN)); |
| | | windowOpener = newWindowOpenerThread(heldBackPeer); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | windowOpener.start(); |
| | | replicationServer.shutdown(); |
| | | final long elapsed = elapsedMillis(startTime); |
| | | |
| | | /* |
| | | * The barrier first, because it is the one the shutdown is made of and it cannot race: |
| | | * the wait ends when both writers have reported, or when the grace period runs out, and |
| | | * the duration below tells the two apart. |
| | | */ |
| | | assertThat(shutdownSync.forwardedBy()) |
| | | .as("the wait ended on the first peer forwarding the message, without the writer of " |
| | | + "the peer which was held back ever reporting one") |
| | | .contains(REMOTE_RS_ID, HELD_BACK_RS_ID); |
| | | assertThat(elapsed).isGreaterThanOrEqualTo(FORWARD_DELAY) |
| | | .isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD); |
| | | assertThat(received.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the peer which was not held back never received the message, its read ended " |
| | | + "with: %s", peer.failure()) |
| | | .isNotNull(); |
| | | /* |
| | | * The forward asserted above proves the message reached the Session, not the wire: close() |
| | | * discards whatever is still in its send queue without draining it, which is the |
| | | * limitation issue #919 recorded. If this is the only assertion which fails, that window |
| | | * is the explanation rather than the granularity of the barrier. |
| | | */ |
| | | assertThat(receivedWhenHeldBack.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the peer which was held back never learned that the replica went offline, " |
| | | + "although its writer reported the forward, its exchange ended with: %s", |
| | | heldBackPeer.failure()) |
| | | .isNotNull(); |
| | | } |
| | | finally |
| | | { |
| | | joinQuietly(windowOpener); |
| | | closeQuietly(heldBackPeer); |
| | | closeQuietly(peer); |
| | | stop(broker); |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A message its writer drops on the way out will never be forwarded, so the shutdown must stop |
| | | * waiting for the peer it was queued for. The filter of the writer is wider than the one |
| | | * ReplicationServerDomain.put() applies when it queues the message - it drops anything for a |
| | | * peer whose generation id is unknown as well - so a peer can be given a message which is then |
| | | * dropped, and nothing would ever report a forward for it. |
| | | */ |
| | | @Test |
| | | public void theShutdownStopsWaitingForAPeerWhoseMessageTheWriterDropped() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | final RecordingShutdownSync shutdownSync = new RecordingShutdownSync(); |
| | | ReplicationServer replicationServer = null; |
| | | ReplicationBroker broker = null; |
| | | FakePeerReplicationServer peer = null; |
| | | try |
| | | { |
| | | final int replicationPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = |
| | | newReplicationServer(shutdownSync, "shutdownSyncDroppedMsgDb", 8230, replicationPort); |
| | | broker = |
| | | openReplicationSession(baseDN, LOCAL_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID); |
| | | peer = new FakePeerReplicationServer( |
| | | replicationPort, REMOTE_RS_ID, baseDN, EMPTY_DN_GENID, HELD_BACK_PEER_WINDOW); |
| | | |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | waitForConnectedReplicationServer(domain, REMOTE_RS_ID); |
| | | final ReplicationServerHandler rsHandler = domain.getConnectedRSs().get(REMOTE_RS_ID); |
| | | |
| | | /* |
| | | * One change fills the send window of the peer, so the message which follows stays with |
| | | * its writer until the window is opened again. The writer takes the message off the queue |
| | | * before it blocks on the permit and evaluates its filter only once it has it, which is |
| | | * what makes the generation id below take effect on a message already queued. |
| | | */ |
| | | final CSNGenerator csns = new CSNGenerator(LOCAL_DS_ID, 0); |
| | | final Future<DeleteMsg> windowFiller = peer.receive(DeleteMsg.class); |
| | | broker.publish(new DeleteMsg(DN.valueOf("uid=offline," + TEST_ROOT_DN_STRING), |
| | | csns.newCSN(), "offline-entry-uuid")); |
| | | assertThat(windowFiller.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the send window of the peer was never filled, its exchange ended with: %s", |
| | | peer.failure()) |
| | | .isNotNull(); |
| | | |
| | | final CSN offlineCSN = csns.newCSN(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN); |
| | | broker.publish(new ReplicaOfflineMsg(offlineCSN)); |
| | | shutdownSync.awaitDispatch(); |
| | | assertThat(shutdownSync.dispatchedTo()) |
| | | .as("the message was never queued for the peer, so its writer has nothing to drop") |
| | | .contains(REMOTE_RS_ID); |
| | | |
| | | // the peer no longer shares the generation id of the domain, so its writer drops what was |
| | | // queued for it before that - a filter wider than the one put() applied |
| | | rsHandler.setGenerationId(domain.getGenerationId() + 1); |
| | | peer.openSendWindow(PEER_WINDOW); |
| | | awaitGiveUpOn(shutdownSync, REMOTE_RS_ID, |
| | | "the writer dropped the message without telling the shutdown to stop waiting for the " |
| | | + "peer it was queued for"); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | replicationServer.shutdown(); |
| | | final long elapsed = elapsedMillis(startTime); |
| | | |
| | | assertThat(elapsed) |
| | | .as("the shutdown kept waiting for a forward its own writer had already dropped") |
| | | .isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD); |
| | | } |
| | | finally |
| | | { |
| | | closeQuietly(peer); |
| | | stop(broker); |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A peer whose handshake is aborted while the message is being pushed must not be waited for. |
| | | * <p> |
| | | * put() reads the peers of the domain, records them as the recipients of the message and only |
| | | * then queues it for each of them. unregisterFailedHandshake() runs on the handshake thread |
| | | * and takes no domain lock, so it can land in between: its own give-up then finds nothing |
| | | * recorded yet and does nothing. Nothing else would ever strike that peer off - the reader and |
| | | * writer threads whose death reaches stopServer() were never started for a handshake which was |
| | | * aborted - so the shutdown waits out its whole grace period for a forward which cannot come. |
| | | */ |
| | | @Test |
| | | public void theShutdownStopsWaitingForAPeerWhoseHandshakeWasAborted() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | final RecordingShutdownSync shutdownSync = new RecordingShutdownSync(); |
| | | ReplicationServer replicationServer = null; |
| | | ReplicationBroker broker = null; |
| | | FakePeerReplicationServer peer = null; |
| | | try (ServerSocket listen = TestCaseUtils.bindFreePort()) |
| | | { |
| | | listen.setSoTimeout(SOCKET_TIMEOUT_MS); |
| | | final int replicationPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = newReplicationServer( |
| | | shutdownSync, "shutdownSyncAbortedHandshakeDb", 8231, 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, REMOTE_RS_ID); |
| | | |
| | | final Session[] sessionPair = connectSessionPair(listen, getReplSessionSecurity()); |
| | | try (Session remoteEnd = sessionPair[0]; |
| | | Session session = sessionPair[1]) |
| | | { |
| | | // a second peer, registered as a handshake does just before it fails |
| | | final ReplicationServerHandler aborting = registerConnectedReplicationServer( |
| | | replicationServer, baseDN, session, ABORTED_RS_ID); |
| | | aborting.setGenerationId(domain.getGenerationId()); |
| | | shutdownSync.runWhileDispatching(new Runnable() |
| | | { |
| | | @Override |
| | | public void run() |
| | | { |
| | | domain.unregisterFailedHandshake(aborting); |
| | | } |
| | | }); |
| | | |
| | | final Future<ReplicaOfflineMsg> received = peer.receive(ReplicaOfflineMsg.class); |
| | | final CSN offlineCSN = newOfflineCSN(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN); |
| | | broker.publish(new ReplicaOfflineMsg(offlineCSN)); |
| | | shutdownSync.awaitDispatch(); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | replicationServer.shutdown(); |
| | | final long elapsed = elapsedMillis(startTime); |
| | | |
| | | assertThat(shutdownSync.dispatchedTo()) |
| | | .as("the peer whose handshake was aborted was not recorded among the recipients, so " |
| | | + "this test never reproduced the window it is about") |
| | | .contains(REMOTE_RS_ID, ABORTED_RS_ID); |
| | | assertThat(elapsed) |
| | | .as("the shutdown waited for a peer whose handshake had been aborted, and which " |
| | | + "nothing else will ever strike off") |
| | | .isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD); |
| | | assertThat(received.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the peer which was still connected never learned that the replica went " |
| | | + "offline, its read ended with: %s", peer.failure()) |
| | | .isNotNull(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | closeQuietly(peer); |
| | | stop(broker); |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A peer which disconnects during the grace period can no longer forward what it was given - |
| | | * its session is closed under its writer - so stopServer() must strike it off rather than let |
| | | * the shutdown wait out the rest of its window for a peer which is already gone. |
| | | */ |
| | | @Test |
| | | public void theShutdownStopsWaitingForAPeerWhichDisconnected() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | final RecordingShutdownSync shutdownSync = new RecordingShutdownSync(); |
| | | ReplicationServer replicationServer = null; |
| | | ReplicationBroker broker = null; |
| | | FakePeerReplicationServer peer = null; |
| | | FakePeerReplicationServer heldBackPeer = null; |
| | | try |
| | | { |
| | | final int replicationPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = newReplicationServer( |
| | | shutdownSync, "shutdownSyncDisconnectedPeerDb", 8232, replicationPort); |
| | | broker = |
| | | openReplicationSession(baseDN, LOCAL_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID); |
| | | peer = new FakePeerReplicationServer( |
| | | replicationPort, REMOTE_RS_ID, baseDN, EMPTY_DN_GENID, PEER_WINDOW); |
| | | heldBackPeer = new FakePeerReplicationServer( |
| | | replicationPort, HELD_BACK_RS_ID, baseDN, EMPTY_DN_GENID, HELD_BACK_PEER_WINDOW); |
| | | |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | waitForConnectedReplicationServer(domain, REMOTE_RS_ID); |
| | | waitForConnectedReplicationServer(domain, HELD_BACK_RS_ID); |
| | | |
| | | // the peer which disconnects is held back by a full send window, so that it cannot have |
| | | // forwarded the message before it goes: what ends the wait must be the give-up |
| | | final CSNGenerator csns = new CSNGenerator(LOCAL_DS_ID, 0); |
| | | final Future<DeleteMsg> windowFiller = heldBackPeer.receive(DeleteMsg.class); |
| | | broker.publish(new DeleteMsg(DN.valueOf("uid=offline," + TEST_ROOT_DN_STRING), |
| | | csns.newCSN(), "offline-entry-uuid")); |
| | | assertThat(windowFiller.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the send window of the held back peer was never filled, its exchange ended " |
| | | + "with: %s", heldBackPeer.failure()) |
| | | .isNotNull(); |
| | | |
| | | final Future<ReplicaOfflineMsg> received = peer.receive(ReplicaOfflineMsg.class); |
| | | final CSN offlineCSN = csns.newCSN(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN); |
| | | broker.publish(new ReplicaOfflineMsg(offlineCSN)); |
| | | shutdownSync.awaitDispatch(); |
| | | heldBackPeer.close(); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | replicationServer.shutdown(); |
| | | final long elapsed = elapsedMillis(startTime); |
| | | |
| | | assertThat(shutdownSync.dispatchedTo()) |
| | | .as("the message was not queued for the peer which then disconnected, so this test " |
| | | + "never reproduced what it is about") |
| | | .contains(REMOTE_RS_ID, HELD_BACK_RS_ID); |
| | | assertThat(elapsed) |
| | | .as("the shutdown kept waiting for a forward from a peer which had disconnected") |
| | | .isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD); |
| | | assertThat(received.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("the peer which stayed connected never received the message, its read ended " |
| | | + "with: %s", peer.failure()) |
| | | .isNotNull(); |
| | | } |
| | | finally |
| | | { |
| | | closeQuietly(heldBackPeer); |
| | | closeQuietly(peer); |
| | | stop(broker); |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A peer which does not share the generation id of the domain is not given the message, so it |
| | | * must not be recorded among the peers the shutdown waits for. The caller side check on |
| | | * getConnectedRSs() does not cover this: such a peer is connected, and would make the domain |
| | | * spend its grace period on a message it was never queued. |
| | | */ |
| | | @Test |
| | | public void theMessageIsNotQueuedForAPeerWhoseGenerationIdDiffers() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | final RecordingShutdownSync shutdownSync = new RecordingShutdownSync(); |
| | | ReplicationServer replicationServer = null; |
| | | ReplicationBroker broker = null; |
| | | FakePeerReplicationServer peer = null; |
| | | try |
| | | { |
| | | final int replicationPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = newReplicationServer( |
| | | shutdownSync, "shutdownSyncOtherGenerationIdDb", 8233, 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, REMOTE_RS_ID); |
| | | domain.getConnectedRSs().get(REMOTE_RS_ID).setGenerationId(domain.getGenerationId() + 1); |
| | | |
| | | final CSN offlineCSN = newOfflineCSN(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN); |
| | | broker.publish(new ReplicaOfflineMsg(offlineCSN)); |
| | | shutdownSync.awaitDispatch(); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | replicationServer.shutdown(); |
| | | final long elapsed = elapsedMillis(startTime); |
| | | |
| | | assertThat(shutdownSync.dispatchedTo()) |
| | | .as("the message was recorded as queued for a peer which does not share the " |
| | | + "generation id of the domain, and which it was never queued for") |
| | | .isEmpty(); |
| | | assertThat(elapsed) |
| | | .as("the shutdown waited for a peer the message was not queued for") |
| | | .isLessThan(DSRSShutdownSync.REPLICA_OFFLINE_GRACE_PERIOD); |
| | | } |
| | | finally |
| | | { |
| | | closeQuietly(peer); |
| | | 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. |
| | | */ |
| | |
| | | replicationServer = |
| | | newReplicationServer(shutdownSync, "shutdownSyncDataServerDb", 8225, replicationPort); |
| | | broker = openReplicationSession(baseDN, REMOTE_DS_ID, 100, replicationPort, 5000, EMPTY_DN_GENID); |
| | | // the writer this test is about exists only once the handshake of the directory server is |
| | | // over, and the shutdown below would otherwise be free to abort that handshake instead |
| | | waitForConnectedDirectoryServer(replicationServer.getReplicationServerDomain(baseDN, true)); |
| | | |
| | | final long startTime = System.nanoTime(); |
| | | shutdownSync.replicaOfflineMsgSent(baseDN, newOfflineCSN()); |
| | |
| | | * 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( |
| | | private ReplicationServerHandler registerConnectedReplicationServer( |
| | | ReplicationServer replicationServer, DN baseDN, Session session) throws Exception |
| | | { |
| | | return registerConnectedReplicationServer(replicationServer, baseDN, session, REMOTE_RS_ID); |
| | | } |
| | | |
| | | private ReplicationServerHandler registerConnectedReplicationServer( |
| | | ReplicationServer replicationServer, DN baseDN, Session session, int serverId) |
| | | throws Exception |
| | | { |
| | | final ReplicationServerDomain domain = replicationServer.getReplicationServerDomain(baseDN, true); |
| | | final ReplicationServerHandler rsHandler = |
| | | new ReplicationServerHandler(session, 100, replicationServer, 100); |
| | | rsHandler.serverId = REMOTE_RS_ID; |
| | | rsHandler.serverId = serverId; |
| | | rsHandler.serverURL = "127.0.0.1:1636"; |
| | | rsHandler.setBaseDNAndDomain(baseDN, false); |
| | | domain.lock(); |
| | |
| | | { |
| | | domain.release(); |
| | | } |
| | | return rsHandler; |
| | | } |
| | | |
| | | private void waitForConnectedReplicationServer(final ReplicationServerDomain domain) |
| | | throws Exception |
| | | /** Waits for the barrier to have been told that this peer will not forward the message. */ |
| | | private void awaitGiveUpOn(final RecordingShutdownSync shutdownSync, final int serverId, |
| | | final String reason) throws Exception |
| | | { |
| | | new TestTimer.Builder() |
| | | .maxSleep(WRITER_REACTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) |
| | | .sleepTimes(10, TimeUnit.MILLISECONDS) |
| | | .toTimer() |
| | | .repeatUntilSuccess(new TestTimer.CallableVoid() |
| | | { |
| | | @Override |
| | | public void call() throws Exception |
| | | { |
| | | assertThat(shutdownSync.gaveUpOn()).as(reason).contains(serverId); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Waits for the peer replication server to be connected <em>and</em> for its handshake to be |
| | | * over. |
| | | * <p> |
| | | * The registration is not the end of the handshake: {@code startFromRemoteRS()} puts the |
| | | * handler in {@code connectedRSs} before it calls {@code finalizeStart()}, which is what |
| | | * starts the reader and the writer, and the whole handshake runs in the listen thread of the |
| | | * replication server. {@link ReplicationServer#shutdown()} interrupts that thread before it |
| | | * waits for the ReplicaOfflineMsgs, so a shutdown triggered while the handshake is still in |
| | | * {@code Session.waitForStartup()} aborts it: the session is closed and the handler |
| | | * unregistered, and the peer is gone before the message could be queued for it, let alone |
| | | * forwarded. Issue #821 recorded that same window, from the dead handler it used to leave |
| | | * behind. |
| | | * <p> |
| | | * The listen thread serves one handshake at a time, so a peer which is past that window also |
| | | * puts every connection accepted before it - the collocated directory server of these tests |
| | | * among them - past it. |
| | | */ |
| | | private void waitForConnectedReplicationServer( |
| | | final ReplicationServerDomain domain, final int serverId) 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); |
| | | final ReplicationServerHandler rsHandler = domain.getConnectedRSs().get(serverId); |
| | | assertThat(rsHandler) |
| | | .as("the peer replication server %s never connected", serverId).isNotNull(); |
| | | assertThat(handshakeIsOver(rsHandler)) |
| | | .as("the handshake of the peer replication server %s never finished", serverId) |
| | | .isTrue(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Waits for the collocated directory server to be connected and for its handshake to be over - |
| | | * {@link #waitForConnectedReplicationServer(ReplicationServerDomain, int)} says what the |
| | | * registration alone leaves open. |
| | | */ |
| | | private DataServerHandler waitForConnectedDirectoryServer(final ReplicationServerDomain domain) |
| | | throws Exception |
| | | { |
| | |
| | | { |
| | | final DataServerHandler dsHandler = domain.getConnectedDSs().get(REMOTE_DS_ID); |
| | | assertThat(dsHandler).as("the directory server never connected").isNotNull(); |
| | | assertThat(handshakeIsOver(dsHandler)) |
| | | .as("the handshake of the directory server never finished").isTrue(); |
| | | return dsHandler; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Whether the handshake of the handler is over, so that the interrupt |
| | | * {@link ReplicationServer#shutdown()} sends to its listen thread can no longer abort it: |
| | | * {@code ServerHandler.finalizeStart()} registers the handler as a monitor provider by its |
| | | * last statement, after the reader and the writer have been started. |
| | | */ |
| | | private static boolean handshakeIsOver(ServerHandler handler) |
| | | { |
| | | return DirectoryServer.getMonitorProviders().containsValue(handler); |
| | | } |
| | | |
| | | private static TestTimer newConnectionTimer() |
| | | { |
| | | return new TestTimer.Builder() |
| | |
| | | { |
| | | return; |
| | | } |
| | | shutdownSync.replicaOfflineMsgForwarded(baseDN, offlineCSN); |
| | | shutdownSync.replicaOfflineMsgForwarded(baseDN, offlineCSN, REMOTE_RS_ID); |
| | | } |
| | | }); |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Gives the held back peer credit to receive again, once the shutdown has had time to end on |
| | | * the forward of the peer which was not held back. |
| | | */ |
| | | private Thread newWindowOpenerThread(final FakePeerReplicationServer heldBackPeer) |
| | | { |
| | | return new Thread(new Runnable() |
| | | { |
| | | @Override |
| | | public void run() |
| | | { |
| | | if (!sleepQuietly(FORWARD_DELAY)) |
| | | { |
| | | return; |
| | | } |
| | | heldBackPeer.openSendWindow(PEER_WINDOW); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private Thread newReAnnouncerThread(final DSRSShutdownSync shutdownSync, final DN baseDN1, |
| | | final DN baseDN2, final AtomicBoolean stopped) |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * A synchronization object which records what the production code reports to it, so that a |
| | | * test can assert on the barrier the shutdown is made of rather than only on how long it took, |
| | | * and can interleave a teardown with the dispatch of a message. |
| | | */ |
| | | private static final class RecordingShutdownSync extends DSRSShutdownSync |
| | | { |
| | | private final List<Integer> dispatchedTo = new CopyOnWriteArrayList<>(); |
| | | private final List<Integer> forwardedBy = new CopyOnWriteArrayList<>(); |
| | | private final List<Integer> gaveUpOn = new CopyOnWriteArrayList<>(); |
| | | private final CountDownLatch dispatched = new CountDownLatch(1); |
| | | /** Runs inside the next dispatch, before the recipients are recorded. */ |
| | | private final AtomicReference<Runnable> whileDispatching = new AtomicReference<>(); |
| | | |
| | | /** |
| | | * Runs the provided action inside the next dispatch, before the recipients are recorded: |
| | | * the window in which put() has read the peers of the domain and nothing knows yet which of |
| | | * them the message is for. |
| | | */ |
| | | void runWhileDispatching(Runnable action) |
| | | { |
| | | whileDispatching.set(action); |
| | | } |
| | | |
| | | @Override |
| | | public void replicaOfflineMsgDispatched( |
| | | DN baseDN, CSN offlineCSN, Collection<Integer> replicationServerIds) |
| | | { |
| | | final Runnable action = whileDispatching.getAndSet(null); |
| | | if (action != null) |
| | | { |
| | | action.run(); |
| | | } |
| | | dispatchedTo.addAll(replicationServerIds); |
| | | super.replicaOfflineMsgDispatched(baseDN, offlineCSN, replicationServerIds); |
| | | dispatched.countDown(); |
| | | } |
| | | |
| | | @Override |
| | | public void replicaOfflineMsgForwarded(DN baseDN, CSN forwardedCSN, int replicationServerId) |
| | | { |
| | | forwardedBy.add(replicationServerId); |
| | | super.replicaOfflineMsgForwarded(baseDN, forwardedCSN, replicationServerId); |
| | | } |
| | | |
| | | @Override |
| | | public void replicaOfflineMsgNotForwarded(DN baseDN, int replicationServerId) |
| | | { |
| | | gaveUpOn.add(replicationServerId); |
| | | super.replicaOfflineMsgNotForwarded(baseDN, replicationServerId); |
| | | } |
| | | |
| | | /** Waits for put() to have pushed a ReplicaOfflineMsg to the peers of its domain. */ |
| | | void awaitDispatch() throws InterruptedException |
| | | { |
| | | assertThat(dispatched.await(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS)) |
| | | .as("no ReplicaOfflineMsg was ever pushed to the peers of the domain") |
| | | .isTrue(); |
| | | } |
| | | |
| | | /** The peers the message was recorded as queued for. */ |
| | | List<Integer> dispatchedTo() |
| | | { |
| | | return dispatchedTo; |
| | | } |
| | | |
| | | /** The peers whose writer reported having forwarded the message. */ |
| | | List<Integer> forwardedBy() |
| | | { |
| | | return forwardedBy; |
| | | } |
| | | |
| | | /** The peers the shutdown was told to stop waiting for. */ |
| | | List<Integer> gaveUpOn() |
| | | { |
| | | return gaveUpOn; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 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; |
| | | /** |
| | | * What ended the exchange with the replication server, so that a message which never arrived |
| | | * can be told from an exchange which failed. The reader and the thread which opens the send |
| | | * window both report here, and the first failure is the one kept: it is the one which |
| | | * explains the rest. |
| | | */ |
| | | private final AtomicReference<Exception> failure = new AtomicReference<>(); |
| | | |
| | | FakePeerReplicationServer(int replicationPort, int serverId, DN baseDN, long generationId) |
| | | throws Exception |
| | | { |
| | | this(replicationPort, serverId, baseDN, generationId, PEER_WINDOW); |
| | | } |
| | | |
| | | FakePeerReplicationServer(int replicationPort, int serverId, DN baseDN, long generationId, |
| | | int windowSize) throws Exception |
| | | { |
| | | final Socket socket = new Socket(); |
| | | Session newSession = null; |
| | | boolean handshaken = false; |
| | |
| | | |
| | | final String serverURL = "127.0.0.1:" + socket.getLocalPort(); |
| | | final byte groupId = (byte) 1; |
| | | newSession.publish(new ReplServerStartMsg(serverId, serverURL, baseDN, 100, |
| | | newSession.publish(new ReplServerStartMsg(serverId, serverURL, baseDN, windowSize, |
| | | new ServerState(), generationId, false, groupId, 5000)); |
| | | final ReplServerStartMsg inStartMsg = |
| | | waitForSpecificMsg(newSession, ReplServerStartMsg.class); |
| | |
| | | session = newSession; |
| | | } |
| | | |
| | | /** Returns the first ReplicaOfflineMsg this peer receives, or null if its session ends first. */ |
| | | Future<ReplicaOfflineMsg> receiveReplicaOfflineMsg() |
| | | /** |
| | | * Returns the first message of the given type this peer receives, or null if its session |
| | | * ends first. |
| | | */ |
| | | <T extends ReplicationMsg> Future<T> receive(final Class<T> msgClass) |
| | | { |
| | | return reader.submit(new Callable<ReplicaOfflineMsg>() |
| | | return reader.submit(new Callable<T>() |
| | | { |
| | | @Override |
| | | public ReplicaOfflineMsg call() |
| | | public T call() |
| | | { |
| | | try |
| | | { |
| | | while (true) |
| | | { |
| | | final ReplicationMsg msg = session.receive(); |
| | | if (msg instanceof ReplicaOfflineMsg) |
| | | if (msgClass.isInstance(msg)) |
| | | { |
| | | return (ReplicaOfflineMsg) msg; |
| | | return msgClass.cast(msg); |
| | | } |
| | | } |
| | | } |
| | |
| | | { |
| | | // The session is closed when the replication server completes its shutdown: whatever |
| | | // has not arrived by then never will. |
| | | readerFailure = e; |
| | | failed(e); |
| | | return null; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** Returns what ended the read of this peer, null if nothing did. */ |
| | | Exception readerFailure() |
| | | /** Gives the replication server credit to publish again, as a peer which keeps up does. */ |
| | | void openSendWindow(int credits) |
| | | { |
| | | return readerFailure; |
| | | try |
| | | { |
| | | session.publish(new WindowMsg(credits)); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | failed(e); |
| | | } |
| | | } |
| | | |
| | | private void failed(Exception e) |
| | | { |
| | | failure.compareAndSet(null, e); |
| | | } |
| | | |
| | | /** Returns what ended the exchange with this peer, null if nothing did. */ |
| | | Exception failure() |
| | | { |
| | | return failure.get(); |
| | | } |
| | | |
| | | void close() |