From 9300ffec887a766b1ccf27198f69f7e5d5e3fb84 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 15 Sep 2026 07:24:43 +0000
Subject: [PATCH] [#956] Tell a failed entryUUID search apart from an entry which is not there (#968)
---
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java | 555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 555 insertions(+), 0 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
index cb74306..29659d1 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
@@ -18,11 +18,14 @@
package org.opends.server.replication.plugin;
import static org.assertj.core.api.Assertions.*;
+import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.core.DirectoryServer.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.testng.Assert.*;
+import java.util.ArrayList;
+import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -33,6 +36,8 @@
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyDNOperation;
+import org.opends.server.core.ModifyOperationBasis;
+import org.opends.server.plugins.ShortCircuitPlugin;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
@@ -40,8 +45,11 @@
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.LDAPUpdateMsg;
import org.opends.server.replication.protocol.ModifyDNMsg;
+import org.opends.server.replication.protocol.ModifyMsg;
+import org.opends.server.replication.protocol.OperationContext;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.types.Entry;
+import org.opends.server.types.OperationType;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -52,6 +60,10 @@
{
private static final AtomicBoolean SHUTDOWN = new AtomicBoolean(false);
+ /** The monitor attributes which count the naming conflicts a domain solved, and did not. */
+ private static final String RESOLVED_NAMING_CONFLICTS = "resolved-naming-conflicts";
+ private static final String UNRESOLVED_NAMING_CONFLICTS = "unresolved-naming-conflicts";
+
private DN baseDN;
private LDAPReplicationDomain domain;
private CSNGenerator gen;
@@ -245,6 +257,531 @@
}
/**
+ * Test case for [Issue 956]: conflict resolution reads the data with a search of the
+ * entryUUID, and a search which did not run is no evidence about the data - the entry
+ * it did not report is not an entry which was deleted.
+ * <p>
+ * The change replayed here was made on the master under a DN this replica does not
+ * have: the entry lives under another one, the way it does after a rename which was
+ * replayed first, and only the entryUUID search finds it. So the change is applied
+ * only if that search is given another chance once the storage serves it again.
+ * Reading its failure as "the entry has been deleted" answers NOTHING_TO_DO, which
+ * records the change as replayed and loses it for good - the replication server never
+ * sends a change this replica reports itself past.
+ */
+ @Test
+ public void modifyIsRetriedWhileTheEntryUUIDSearchCanNotRun() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modifyWhoseSearchCanNotRun");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final String phoneNumber = "01 02 45";
+
+ // The DN the change carries is the one the entry had on the master. Here the entry
+ // is the one which was just added, which only the entryUUID search finds.
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final CSN csn = gen.newCSN();
+
+ /*
+ * The storage does not serve the search for the first attempts and serves it after
+ * them: a failure which lasts less than the attempts made in place, the way a
+ * backend which is being rebuilt or a connection which was lost does. The short
+ * circuit is put in force right before the replay and dropped right after it - it
+ * applies to every search of this server while it is registered, and the replay
+ * here runs on this thread.
+ */
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 2);
+ try
+ {
+ replayMsg(new ModifyMsg(csn, staleDN, generatemods("telephonenumber", phoneNumber), entryUUID));
+ assertShortCircuitSpentBy(2);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ final Entry replayedEntry = DirectoryServer.getEntry(entry.getName());
+ assertEquals(replayedEntry.parseAttribute("telephonenumber").asString(), phoneNumber,
+ "the change was not applied: a search which could not run was read as a deleted entry");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ }
+
+ /**
+ * Test case for [Issue 956]: the entryUUID searches which check a replayed Add for a
+ * conflict read the data the same way, and a search which did not run is no evidence
+ * about it either. The first of them checks whether the Add was replayed here already.
+ * <p>
+ * The Add is delivered a second time - the replication server sends again what a
+ * replica does not report itself past - and the entry was renamed here since it was
+ * added: only the entryUUID search finds it. An entry that search did not report is
+ * not an entry which is not there: reading it that way adds the entry a second time,
+ * under its former DN, and the data holds one entryUUID twice.
+ */
+ @Test
+ public void addIsNotReplayedTwiceWhileTheEntryUUIDSearchCanNotRun() throws Exception
+ {
+ final Entry entry = createAndAddEntry("addWhoseSearchCanNotRun");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final RDN renamedRDN = RDN.valueOf("cn=renamedAfterTheAdd");
+ final ModifyDNOperation rename =
+ getRootConnection().processModifyDN(entry.getName(), renamedRDN, true);
+ assertEquals(rename.getResultCode(), ResultCode.SUCCESS);
+ final CSN csn = gen.newCSN();
+
+ // The first attempt fails its first search; the second attempt has it served.
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 1);
+ try
+ {
+ replayMsg(addMsg(entry, csn, getEntryUUID(baseDN), entryUUID));
+ assertShortCircuitSpentBy(1);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertFalse(entryExists(entry.getName()),
+ "the entry was added a second time: a search which could not run was read as an "
+ + "Add which was not replayed here yet");
+ assertTrue(entryExists(baseDN.child(renamedRDN)), "the renamed entry is gone");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which is in the data must be recorded as replayed");
+ }
+
+ /**
+ * Test case for [Issue 956]: the search which checks that the parent of a replayed
+ * Add is still the one the change was made under fails, and the one before it - the
+ * check that the Add was not replayed here already - ran.
+ * <p>
+ * A parent that search did not report is not a parent which was deleted: the Add is
+ * attempted again once the storage serves the search, and no naming conflict is
+ * counted for a search which read nothing. Reading it as a parent which is gone hands
+ * the Add to conflict resolution as the naming conflict it is not - and renames the
+ * entry under the base DN as a conflicting entry, a divergence which is left for an
+ * administrator to repair by hand, when the search conflict resolution makes fails as
+ * well.
+ */
+ @Test
+ public void addIsRetriedWhileTheParentEntryUUIDSearchCanNotRun() throws Exception
+ {
+ final Entry parent = addParentEntry("addWhoseParentSearchCanNotRun");
+ final String parentUUID = getEntryUUID(parent.getName());
+ final Entry child = makeChildEntry("addedWhileTheParentSearchFailed", parent.getName());
+ final CSN csn = gen.newCSN();
+ final long resolvedConflicts = getMonitorAttrValue(baseDN, RESOLVED_NAMING_CONFLICTS);
+ final long unresolvedConflicts = getMonitorAttrValue(baseDN, UNRESOLVED_NAMING_CONFLICTS);
+
+ /*
+ * The first search of the first attempt - the check for an Add replayed already - is
+ * let through, the parent check right after it fails, and every search after that
+ * one is served. A parent search read as a parent which is gone hands the Add to
+ * conflict resolution, whose own search finds the parent where it was and rewrites
+ * the message to the DN the Add already carries: the entry lands where it should
+ * either way, and what tells the two apart is the naming conflict counted for a
+ * search which read nothing.
+ */
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 1, 1);
+ try
+ {
+ replayMsg(addMsg(child, csn, parentUUID, "1c3c2c4d-2b5e-4b9f-8a7c-3d5e6f7a8b9c"));
+ assertShortCircuitSpentBy(2);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertTrue(entryExists(child.getName()),
+ "the entry was not added under its parent: a parent search which could not run "
+ + "was read as a parent which is gone");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ assertEquals(getMonitorAttrValue(baseDN, RESOLVED_NAMING_CONFLICTS), resolvedConflicts,
+ "a search which did not run is not a naming conflict which was resolved");
+ assertEquals(getMonitorAttrValue(baseDN, UNRESOLVED_NAMING_CONFLICTS), unresolvedConflicts,
+ "a search which did not run is not a naming conflict which could not be resolved");
+ }
+
+ /**
+ * Test case for [Issue 956]: the search conflict resolution reads the data with once a
+ * replayed Add failed on a genuine conflict fails, and the checks before the Add ran.
+ * <p>
+ * The parent of the entry was renamed here, so the Add carries a DN which is not
+ * where the parent is anymore: a conflict which is solved by adding the entry under
+ * the parent's current DN, once the search which finds that DN runs. A search which
+ * did not run is not a parent which is gone, and the conflict is counted once, when
+ * it is solved - not for the search which read nothing.
+ */
+ @Test
+ public void addIsRetriedWhileTheConflictResolutionSearchCanNotRun() throws Exception
+ {
+ final Entry parent = addParentEntry("addWhoseConflictSearchCanNotRun");
+ final String parentUUID = getEntryUUID(parent.getName());
+ final Entry child = makeChildEntry("addedWhileTheConflictSearchFailed", parent.getName());
+ final String entryUUID = "2d4d3d5e-3c6f-4ca0-9b8d-4e6f7a8b9cad";
+ final CSN csn = gen.newCSN();
+
+ final RDN renamedParentRDN = RDN.valueOf("ou=renamedBeforeTheAdd");
+ final ModifyDNOperation renameParent =
+ getRootConnection().processModifyDN(parent.getName(), renamedParentRDN, true);
+ assertEquals(renameParent.getResultCode(), ResultCode.SUCCESS);
+ final DN expectedDN = baseDN.child(renamedParentRDN).child(child.getName().rdn());
+ final long resolvedConflicts = getMonitorAttrValue(baseDN, RESOLVED_NAMING_CONFLICTS);
+ final long unresolvedConflicts = getMonitorAttrValue(baseDN, UNRESOLVED_NAMING_CONFLICTS);
+
+ /*
+ * The two searches which check the Add before it runs are let through - they find
+ * the parent under its new DN, which is what fails the Add on a conflict - and the
+ * search conflict resolution then reads the data with is the one which fails. The
+ * next attempt has every search served.
+ */
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 2, 1);
+ try
+ {
+ replayMsg(addMsg(child, csn, parentUUID, entryUUID));
+ assertShortCircuitSpentBy(3);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertTrue(entryExists(expectedDN),
+ "the entry was not added under the current DN of its parent: a search which could "
+ + "not run was read as a parent which is gone");
+ // A parent read as gone puts the entry under the base DN as a conflicting entry, with
+ // its entryUUID added to its RDN.
+ assertFalse(entryExists(DN.valueOf(
+ "entryuuid=" + entryUUID + "+" + child.getName().rdn() + "," + TEST_ROOT_DN_STRING)),
+ "the entry was renamed under the base DN as a conflicting entry");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ assertEquals(getMonitorAttrValue(baseDN, RESOLVED_NAMING_CONFLICTS), resolvedConflicts + 1,
+ "the renamed parent is one naming conflict, solved once the search ran");
+ assertEquals(getMonitorAttrValue(baseDN, UNRESOLVED_NAMING_CONFLICTS), unresolvedConflicts,
+ "a search which did not run is not a naming conflict which could not be resolved");
+ }
+
+ /**
+ * Test case for [Issue 956]: a replayed Delete reads the data with the same search
+ * once it failed on a conflict, and rides on the same retry.
+ * <p>
+ * The entry was renamed here, so the Delete carries a DN which is not the entry's
+ * anymore, and only the entryUUID search finds it. Reading the search's failure as an
+ * entry which was deleted already answers NOTHING_TO_DO: the entry stays, and the
+ * change is recorded as replayed.
+ */
+ @Test
+ public void deleteIsRetriedWhileTheEntryUUIDSearchCanNotRun() throws Exception
+ {
+ final Entry entry = createAndAddEntry("deleteWhoseSearchCanNotRun");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final CSN csn = gen.newCSN();
+
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 2);
+ try
+ {
+ replayMsg(new DeleteMsg(staleDN, csn, entryUUID));
+ assertShortCircuitSpentBy(2);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertFalse(entryExists(entry.getName()),
+ "the entry was not deleted: a search which could not run was read as an entry "
+ + "which was deleted already");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ }
+
+ /**
+ * Test case for [Issue 956]: a replayed Modify DN reads the data with the same search
+ * once it failed on a conflict, and rides on the same retry.
+ * <p>
+ * {@code solveNamingConflict(ModifyDNOperation)} declares {@code throws Exception}
+ * rather than the search failure alone, so this is the one path where the failure
+ * reaches the replay loop through a declaration which does not name it.
+ */
+ @Test
+ public void modifyDnIsRetriedWhileTheEntryUUIDSearchCanNotRun() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modifyDnWhoseSearchCanNotRun");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final RDN newRDN = RDN.valueOf("cn=renamedWhileTheSearchFailed");
+ final CSN csn = gen.newCSN();
+
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 2);
+ try
+ {
+ replayMsg(new ModifyDNMsg(staleDN, csn, entryUUID, null, false, null, newRDN.toString()));
+ assertShortCircuitSpentBy(2);
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertTrue(entryExists(baseDN.child(newRDN)),
+ "the entry was not renamed: a search which could not run was read as an entry "
+ + "which is not in the data anymore");
+ assertFalse(entryExists(entry.getName()), "the entry kept its former DN");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ }
+
+ /**
+ * Test case for [Issue 956], the half which closes [Issue 889] for this path: a
+ * change whose entryUUID search never runs is left out of the ServerState once the
+ * attempts in place are spent, so that the replication server sends it again.
+ * <p>
+ * The result code of every attempt is the conflict the operation failed on, which the
+ * exhaustion exit does not read as a failure of the server: without the attempt itself
+ * telling that its search did not run, the exit reads the attempts as conflict
+ * resolution rewriting an operation which keeps failing, and skips the change - the
+ * CSN is committed, and a change which is not in the data is never asked for again.
+ */
+ @Test
+ public void modifyIsLeftOutOfTheServerStateWhenTheEntryUUIDSearchNeverRuns() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modifyWhoseSearchNeverRuns");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final CSN csn = gen.newCSN();
+
+ // No number of times: every attempt in place fails its search.
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue());
+ try
+ {
+ replayMsg(new ModifyMsg(csn, staleDN, generatemods("telephonenumber", "01 02 45"), entryUUID));
+ assertTrue(ShortCircuitPlugin.getShortCircuitCount(OperationType.SEARCH, "PreParse")
+ >= LDAPReplicationDomain.IN_PLACE_REPLAY_ATTEMPTS,
+ "every attempt in place must have made its search");
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertFalse(domain.getServerState().cover(csn),
+ "a change whose search never ran is not in the data and must not advance the ServerState");
+ assertThat(DirectoryServer.getEntry(entry.getName()).getAllAttributes("telephonenumber"))
+ .as("the change was applied to the entry the searches never found").isEmpty();
+ /*
+ * The result code of the last attempt is the conflict the operation failed on, which
+ * says nothing of the search: the line which reports the change must carry the search
+ * which did not run, which is the one thing that names the entryUUID it was made for.
+ */
+ final List<String> reports = exhaustionExitRecordsOf(csn);
+ assertThat(reports).as("the change was not reported once the attempts in place were spent")
+ .isNotEmpty();
+ assertThat(reports)
+ .as("the exhaustion exit reports the error of the operation, not the search which did not run")
+ .allMatch(record -> record.contains(entryUUID));
+ }
+
+ /**
+ * Test case for [Issue 956]: the exhaustion exit reports the attempt which spent the
+ * last of the attempts in place, not an earlier one which ended on a search conflict
+ * resolution could not run.
+ * <p>
+ * The first attempt reaches the data, fails on the conflict, and its search does not
+ * run; the server then refuses every attempt after it before the data is reached. Each
+ * of these is a failure of the server, and the change is left out of the ServerState
+ * either way - what the line which reports it carries is the question. Its result
+ * code is the last attempt's, and so must be the error next to it: a line which reads
+ * the server refusing the operation next to a search which did not run names two
+ * causes, and the operator chases the wrong one.
+ */
+ @Test
+ public void theExhaustionExitReportsTheAttemptWhichSpentTheLastOfThem() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modifyWhoseLastAttemptIsRefused");
+ final String entryUUID = getEntryUUID(entry.getName());
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final CSN csn = gen.newCSN();
+
+ /*
+ * The first attempt is let through to the data and fails on the conflict of the
+ * stale DN; its search is the one the short circuit stops. The attempts after it are
+ * refused before they reach the data, the way a backend which went offline after the
+ * first attempt refuses them: no search is made on any of them.
+ */
+ ShortCircuitPlugin.registerShortCircuit(
+ OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue(), 1);
+ // The replayed operation only, named by its CSN: see the flush below.
+ ShortCircuitPlugin.registerShortCircuit(OperationType.MODIFY, "PreParse",
+ ResultCode.UNAVAILABLE.intValue(), 1, LDAPReplicationDomain.IN_PLACE_REPLAY_ATTEMPTS - 1,
+ op -> csn.equals(OperationContext.getCSN(op)));
+ try
+ {
+ /*
+ * The ServerState flush thread saves the state with a Modify of the base entry on
+ * its tick, which the add above made dirty: a tick which lands among the attempts in
+ * place is a Modify the short circuit meets like any other. Made here rather than
+ * left to the tick, so that the case says what it does about it every time instead
+ * of once in a while: that Modify is not the replayed operation, and the short
+ * circuit must neither let it through in place of the first attempt nor refuse it
+ * in place of a later one.
+ */
+ flushLikeTheStateFlushThread();
+ assertEquals(ShortCircuitPlugin.getShortCircuitCount(OperationType.MODIFY, "PreParse"), 0,
+ "the Modify of the state flush thread was counted against the short circuit");
+
+ replayMsg(new ModifyMsg(csn, staleDN, generatemods("telephonenumber", "01 02 45"), entryUUID));
+ assertTrue(ShortCircuitPlugin.getShortCircuitCount(OperationType.SEARCH, "PreParse") >= 1,
+ "the first attempt must have made its search");
+ assertTrue(ShortCircuitPlugin.getShortCircuitCount(OperationType.MODIFY, "PreParse")
+ >= LDAPReplicationDomain.IN_PLACE_REPLAY_ATTEMPTS,
+ "every attempt in place must have been made");
+ }
+ finally
+ {
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.MODIFY, "PreParse");
+ ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse");
+ }
+
+ assertFalse(domain.getServerState().cover(csn),
+ "a change the server kept refusing is not in the data and must not advance the ServerState");
+ final List<String> reports = exhaustionExitRecordsOf(csn);
+ assertThat(reports).as("the change was not reported once the attempts in place were spent")
+ .isNotEmpty();
+ // The last attempt was refused before it reached the data and made no search: a line
+ // which names the entryUUID reports the search of an earlier attempt next to its result.
+ assertThat(reports).as("the exhaustion exit reports an attempt other than the last one")
+ .allMatch(record -> !record.contains(entryUUID));
+ }
+
+ /**
+ * Test case for [Issue 956]: the base entry of the domain replayed into a replica
+ * which has none.
+ * <p>
+ * Two empty replicas share the generation ID of an empty backend, so no initialization
+ * is needed and the first change replayed is the base entry itself. The searches which
+ * check that Add for a conflict run under the base DN, which the backend serves and
+ * has no entry for: they answer NO_SUCH_OBJECT, which is the answer of a backend which
+ * is offline as well. Here the search ran and nothing is below a base entry which is
+ * not there, so the Add must go through rather than be retried until the give-up
+ * budget skips it as a change this replica can not apply.
+ */
+ @Test
+ public void baseEntryIsAddedToAnEmptyReplica() throws Exception
+ {
+ // The backend, without its base entry.
+ TestCaseUtils.initializeTestBackend(false);
+ final Entry base = TestCaseUtils.makeEntry(
+ "dn: " + TEST_ROOT_DN_STRING,
+ "objectClass: top",
+ "objectClass: organization",
+ "o: test");
+ final CSN csn = gen.newCSN();
+
+ replayMsg(addMsg(base, csn, null, "7c1a0d2e-4b6f-4c8a-9e1d-3f5b7a9c1e2d"));
+
+ assertTrue(entryExists(base.getName()),
+ "the base entry of an empty replica must land: its searches found nothing, they did not fail");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ }
+
+ /**
+ * Test case for [Issue 956]: the entryUUID a change carries is looked up as a value,
+ * not read as a filter.
+ * <p>
+ * The entryUUID comes off the wire and nothing validates it as one. Built into a
+ * filter string, a value which does not parse as a filter is a search which never
+ * runs - a permanent condition retried as a transient one, for as long as the change
+ * is asked for, until the give-up budget skips it and raises an alert. Looked up as a
+ * value, such an entryUUID names no entry, which is what a search which ran and found
+ * nothing says: the change is resolved as one on an entry which is not in the data.
+ */
+ @Test
+ public void anEntryUUIDWhichIsNotOneNamesNoEntry() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modifyWhoseEntryUUIDIsNotOne");
+ // A value no filter string parses: the backslash escapes nothing.
+ final String entryUUID = getEntryUUID(entry.getName()) + "\\";
+ final DN staleDN = DN.valueOf("cn=movedAway," + TEST_ROOT_DN_STRING);
+ final CSN csn = gen.newCSN();
+
+ replayMsg(new ModifyMsg(csn, staleDN, generatemods("telephonenumber", "01 02 45"), entryUUID));
+
+ assertTrue(domain.getServerState().cover(csn),
+ "a change on an entry which is not in the data is a conflict which is resolved, and recorded");
+ assertThat(DirectoryServer.getEntry(entry.getName()).getAllAttributes("telephonenumber"))
+ .as("a change on an entryUUID which is not one was applied to an entry").isEmpty();
+ }
+
+ /**
+ * The records of the error log which report the provided change once its attempts in
+ * place were spent. The record the error logger writes carries the id of the message
+ * rather than its text.
+ */
+ private static List<String> exhaustionExitRecordsOf(CSN csn)
+ {
+ final String exhaustionExit = "msgID=" + ERR_ERROR_REPLAYING_OPERATION.ordinal();
+ final List<String> reports = new ArrayList<>();
+ for (String record : TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+ {
+ if (record.contains(exhaustionExit) && record.contains(csn.toString()))
+ {
+ reports.add(record);
+ }
+ }
+ return reports;
+ }
+
+ /**
+ * Asserts that the searches a short circuit was registered over were made - the ones
+ * let through before it and the ones it applied to - and that the search after them
+ * was made as well.
+ * <p>
+ * The count includes the searches let through once the short circuit was spent, so a
+ * count past what it was registered over says that the budget was used and that the
+ * search after it ran. Read before the short circuit is deregistered, which drops the
+ * count with it.
+ *
+ * @param searchesRegisteredOver the searches let through before the short circuit plus
+ * the ones it applied to
+ */
+ private void assertShortCircuitSpentBy(int searchesRegisteredOver)
+ {
+ assertTrue(
+ ShortCircuitPlugin.getShortCircuitCount(OperationType.SEARCH, "PreParse") > searchesRegisteredOver,
+ "the short circuit must have been spent by the attempts in place");
+ }
+
+ private Entry addParentEntry(String ou) throws Exception
+ {
+ return TestCaseUtils.addEntry(
+ "dn: ou=" + ou + "," + TEST_ROOT_DN_STRING,
+ "objectClass: top",
+ "objectClass: organizationalUnit",
+ "ou: " + ou);
+ }
+
+ private Entry makeChildEntry(String cn, DN parentDN) throws Exception
+ {
+ return TestCaseUtils.makeEntry(
+ "dn: cn=" + cn + "," + parentDN,
+ "objectClass: top",
+ "objectClass: person",
+ "cn: " + cn,
+ "sn: Amar");
+ }
+
+ /**
* Test that when a previous conflict is resolved because
* a delete operation has removed one of the conflicting entries
* the other conflicting entry is correctly renamed to its original name.
@@ -317,6 +854,24 @@
assertThat(resultEntry.getAllAttributes(LDAPReplicationDomain.DS_SYNC_CONFLICT)).isEmpty();
}
+ /**
+ * Makes the Modify the ServerState flush thread makes on its tick: an internal
+ * synchronization Modify of the base entry, which is not synchronized itself. The
+ * attribute is a harmless one rather than ds-sync-state, which is the flush thread's to
+ * write.
+ */
+ private void flushLikeTheStateFlushThread()
+ {
+ final ModifyOperationBasis op = new ModifyOperationBasis(getRootConnection(),
+ nextOperationID(), nextMessageID(), null,
+ baseDN, generatemods("description", "written on the tick of the flush thread"));
+ op.setInternalOperation(true);
+ op.setSynchronizationOperation(true);
+ op.setDontSynchronize(true);
+ op.run();
+ assertEquals(op.getResultCode(), ResultCode.SUCCESS, op.getErrorMessage().toString());
+ }
+
private Entry createAndAddEntry(String commonName) throws Exception
{
// @formatter:off
--
Gitblit v1.10.0