From f3076a0a7b60fa444dbaea5a69f40ab423562765 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 10 Sep 2026 13:23:09 +0000
Subject: [PATCH] [#955] Answer a ModifyDN whose entry is gone before the new superior is looked up (#965)

---
 opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java    |   40 ++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java |   26 +++++++-----
 2 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
index 4ad0c39..c736131 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -3629,6 +3629,21 @@
   // get the current DN of this entry in the database.
   DN currentDN = findEntryDN(entryUUID);
 
+  if (currentDN == null)
+  {
+    /*
+     * The entry targeted by the Modify DN is not in the database anymore.
+     * This is a conflict between a delete and this modify DN.
+     * The entry has been deleted, we can safely assume that the operation is completed.
+     *
+     * This is answered before the new superior is looked up, and before the branch which
+     * marks the entry as conflicting: an entry which is not in the database can not be
+     * marked, and the delete has already settled what this Modify DN was trying to do.
+     */
+    numResolvedNamingConflicts.incrementAndGet();
+    return ConflictResolution.NOTHING_TO_DO;
+  }
+
   // Construct the new DN to use for the entry.
   DN entryDN = op.getEntryDN();
   DN newSuperior;
@@ -3657,17 +3672,6 @@
 
   DN newDN = newSuperior.child(newRDN);
 
-  if (currentDN == null)
-  {
-    // The entry targeted by the Modify DN is not in the database
-    // anymore.
-    // This is a conflict between a delete and this modify DN.
-    // The entry has been deleted, we can safely assume
-    // that the operation is completed.
-    numResolvedNamingConflicts.incrementAndGet();
-    return ConflictResolution.NOTHING_TO_DO;
-  }
-
   // if the newDN and the current DN match then the operation
   // is a no-op (this was probably a second replay)
   // don't do anything.
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 65c4498..cb74306 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
@@ -205,6 +205,46 @@
   }
 
   /**
+   * Test case for [Issue 955]: a ModifyDN whose entry and whose new superior are both
+   * gone from this replica is a conflict between a delete and this ModifyDN, and it is
+   * solved as such rather than left to be delivered again until this replica gives up on
+   * it.
+   * <p>
+   * Neither entryUUID the message carries resolves to a DN here, so conflict resolution
+   * has no new superior to move the entry under - and no entry to move either. The entry
+   * having been deleted settles what the ModifyDN was trying to do, which is what makes
+   * the change resolved: an entry which is not in the database can not be marked as
+   * conflicting, and marking it is what used to be attempted first, on the DN of an entry
+   * which is not there.
+   */
+  @Test
+  public void modifyDnOnAnEntryAndANewSuperiorWhichAreBothGone() throws Exception
+  {
+    final Entry entry = createAndAddEntry("modDnOnEntryAndNewSuperiorBothGone");
+    final String entryUUID = getEntryUUID(entry.getName());
+
+    final Entry newSuperior = TestCaseUtils.addEntry(
+        "dn: ou=newSuperiorBothGone," + TEST_ROOT_DN_STRING,
+        "objectClass: top",
+        "objectClass: organizationalUnit",
+        "ou: newSuperiorBothGone");
+    final String newSuperiorUUID = getEntryUUID(newSuperior.getName());
+
+    // Both entries are deleted on this replica while the ModifyDN is on its way.
+    TestCaseUtils.deleteEntry(newSuperior.getName());
+    TestCaseUtils.deleteEntry(entry.getName());
+
+    final CSN csn = gen.newCSN();
+    replayMsg(new ModifyDNMsg(entry.getName(), csn, entryUUID, newSuperiorUUID, false,
+        newSuperior.getName().toString(), entry.getName().rdn().toString()));
+
+    assertFalse(entryExists(entry.getName()),
+        "the deleted entry was brought back by the replayed ModifyDN");
+    assertTrue(domain.getServerState().cover(csn),
+        "a ModifyDN which the delete of its entry has settled must be recorded as replayed");
+  }
+
+  /**
    * 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.

--
Gitblit v1.10.0