mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
17 hours ago 9300ffec887a766b1ccf27198f69f7e5d5e3fb84
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