| New file |
| | |
| | | /* |
| | | * 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.plugin; |
| | | |
| | | import static org.mockito.Matchers.any; |
| | | 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 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.protocol.LDAPUpdateMsg; |
| | | import org.opends.server.replication.protocol.ReplicaOfflineMsg; |
| | | import org.opends.server.replication.protocol.UpdateMsg; |
| | | import org.opends.server.replication.service.ReplicationDomain; |
| | | import org.opends.server.types.operation.PluginOperation; |
| | | 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 only reported as sent when |
| | | * it really was. |
| | | * <p> |
| | | * These tests need no server: the changes are built by a CSNGenerator, which reads the time |
| | | * service, and the time service is up as soon as its class is loaded. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | @Test(groups = { "precommit", "replication" }, sequential = true) |
| | | public class PendingChangesTest extends DirectoryServerTestCase |
| | | { |
| | | private static final int SERVER_ID = 42; |
| | | |
| | | @Test |
| | | public void replicaOfflineMsgIsSentWhenNoChangeIsPending() throws Exception |
| | | { |
| | | final ReplicationDomain domain = mock(ReplicationDomain.class); |
| | | 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 message carries the newest CSN of the replica, so a change which is still in flight |
| | | * holds it back - and what was never published 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 reaches the wire. |
| | | */ |
| | | @Test |
| | | public void replicaOfflineMsgQueuedBehindAnUncommittedChangeIsNotReportedAsSent() throws Exception |
| | | { |
| | | final ReplicationDomain domain = mock(ReplicationDomain.class); |
| | | 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 = mock(ReplicationDomain.class); |
| | | 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); |
| | | } |
| | | |
| | | private PendingChanges newPendingChanges(ReplicationDomain domain) |
| | | { |
| | | return new PendingChanges(new CSNGenerator(SERVER_ID, 0), 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(); |
| | | } |
| | | } |