/* * 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 2009-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.replication.plugin; import static org.assertj.core.api.Assertions.*; 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.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.RDN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.server.config.meta.ReplicationDomainCfgDefn.IsolationPolicy; import org.opends.server.TestCaseUtils; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; 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.protocol.AddMsg; 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.UpdateMsg; import org.opends.server.types.Entry; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** Test the naming conflict resolution code. */ @SuppressWarnings("javadoc") public class NamingConflictTest extends ReplicationTestCase { private static final AtomicBoolean SHUTDOWN = new AtomicBoolean(false); private DN baseDN; private LDAPReplicationDomain domain; private CSNGenerator gen; private TestSynchronousReplayQueue queue; /** * The result code to put back in {@code ds-cfg-server-error-result-code}, or * {@code null} when this test did not change it. *
* The setting is server-wide, so it is put back by {@link #tearDown()} rather than by
* the test which changed it: a method the harness kills on its timeout, or interrupts
* inside a replay, would otherwise leave every later method of this class replaying
* against a result code it never asked for.
*/
private Integer serverErrorResultCodeToRestore;
@BeforeMethod
public void setUpLocal() throws Exception
{
baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
TestCaseUtils.initializeTestBackend(true);
queue = new TestSynchronousReplayQueue();
final DomainFakeCfg conf = new DomainFakeCfg(baseDN, 1, new TreeSet
* That setting is a plain integer which is not validated as a result code, so it can be
* one of them. Here it is {@code UNWILLING_TO_PERFORM}, which is what a ModifyDN whose
* new superior is - on this replica - a subordinate of the entry being moved comes back
* with, and only conflict resolution can turn such a change into an operation which
* applies: it resolves both DNs again from the entryUUIDs the message carries. Reading
* the code as a failure of the server would take the change away from it - the message
* would never be rewritten, so no attempt would apply any better than the first - and
* the change would be retried in place, delivered again and finally given up on, with
* the entry left where it was.
*
* {@code UpdateOperationTest.changeConflictResolutionCanNotSolveOnTheServerErrorCodeIsRetried}
* covers the other half: a change which fails with that same code and which conflict
* resolution can not solve is retried as the failure of the server it is.
*/
@Test
public void modifyDnConflictIsSolvedWhileTheServerErrorCodeIsOneOfTheConflictCodes() throws Exception
{
final Entry entry = createAndAddEntry("modDnOnConflictingServerErrorCode");
final String entryUUID = getEntryUUID(entry.getName());
final Entry newParent = TestCaseUtils.addEntry(
"dn: ou=newParent," + TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: organizationalUnit",
"ou: newParent");
final String newParentUUID = getEntryUUID(newParent.getName());
// Remembered for tearDown() rather than restored here: the code which is in force,
// which is the defined default unless the suite configured another one, and never a
// value hardcoded by this test.
serverErrorResultCodeToRestore =
getServerContext().getCoreConfigManager().getServerErrorResultCode().intValue();
setServerErrorResultCode(ResultCode.UNWILLING_TO_PERFORM.intValue());
/*
* The new superior as the master knew it: a DN which, here, is a subordinate of the
* entry being moved - as it would be after that parent was renamed on this replica.
* The operation reports UNWILLING_TO_PERFORM for as long as the message carries that
* DN - ERR_MODDN_NEW_SUPERIOR_IN_SUBTREE, and the memory backend answers the same on
* a new superior which is not in it - so the change is applied only if conflict
* resolution gets to rewrite the message with the DNs the entryUUIDs resolve to here.
*/
final String staleNewSuperior = "ou=newParent," + entry.getName();
final CSN csn = gen.newCSN();
replayMsg(new ModifyDNMsg(entry.getName(), csn, entryUUID, newParentUUID, false,
staleNewSuperior, entry.getName().rdn().toString()));
final DN resolvedDN = newParent.getName().child(entry.getName().rdn());
assertTrue(entryExists(resolvedDN),
"the naming conflict was not solved: no entry at " + resolvedDN);
assertFalse(entryExists(entry.getName()), "the entry was not moved by the replayed ModifyDN");
assertTrue(domain.getServerState().cover(csn),
"a change which was applied 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.
*/
@Test
public void conflictCleaningDelete() throws Exception
{
Entry entry = createAndAddEntry("conflictCleaningDelete");
// Add the first entry
String parentUUID = getEntryUUID(baseDN);
CSN csn1 = gen.newCSN();
// Now try to add the same entry with same DN but a different
// unique ID though the replication
replayMsg(addMsg(entry, csn1, parentUUID, "c9cb8c3c-615a-4122-865d-50323aaaed48"));
// Now delete the first entry that was added at the beginning
TestCaseUtils.deleteEntry(entry.getName());
// Expect the conflict resolution : the second entry should now
// have been renamed with the original DN.
Entry resultEntry = DirectoryServer.getEntry(entry.getName());
assertNotNull(resultEntry, "The conflict was not cleared");
assertEquals(getEntryUUID(resultEntry.getName()),
"c9cb8c3c-615a-4122-865d-50323aaaed48",
"The wrong entry has been renamed");
assertThat(resultEntry.getAllAttributes(LDAPReplicationDomain.DS_SYNC_CONFLICT)).isEmpty();
}
private AddMsg addMsg(Entry entry, CSN csn, String parentUUID, String childUUID)
{
return new AddMsg(csn,
entry.getName(),
childUUID, parentUUID,
entry.getObjectClasses(), entry.getUserAttributes(),
null);
}
/**
* Test that when a previous conflict is resolved because
* a MODDN operation has removed one of the conflicting entries
* the other conflicting entry is correctly renamed to its original name.
*/
@Test
public void conflictCleaningMODDN() throws Exception
{
Entry entry = createAndAddEntry("conflictCleaningDelete");
String parentUUID = getEntryUUID(baseDN);
CSN csn1 = gen.newCSN();
// Now try to add the same entry with same DN but a different
// unique ID though the replication
replayMsg(addMsg(entry, csn1, parentUUID, "c9cb8c3c-615a-4122-865d-50323aaaed48"));
// Now delete the first entry that was added at the beginning
ModifyDNOperation modDNOperation =
getRootConnection().processModifyDN(entry.getName(), RDN.valueOf("cn=foo"), false);
assertEquals(modDNOperation.getResultCode(), ResultCode.SUCCESS);
// Expect the conflict resolution : the second entry should now
// have been renamed with the original DN.
Entry resultEntry = DirectoryServer.getEntry(entry.getName());
assertNotNull(resultEntry, "The conflict was not cleared");
assertEquals(getEntryUUID(resultEntry.getName()),
"c9cb8c3c-615a-4122-865d-50323aaaed48",
"The wrong entry has been renamed");
assertThat(resultEntry.getAllAttributes(LDAPReplicationDomain.DS_SYNC_CONFLICT)).isEmpty();
}
private Entry createAndAddEntry(String commonName) throws Exception
{
// @formatter:off
return TestCaseUtils.addEntry(
"dn: cn=" + commonName + ", " + TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: user.1",
"description: This is the description for Aaccf Amar.",
"st: NC",
"postalAddress: Aaccf Amar$17984 Thirteenth Street$Rockford, NC 85762",
"mail: user.1@example.com",
"cn: Aaccf Amar",
"l: Rockford",
"street: 17984 Thirteenth Street",
"employeeNumber: 1",
"sn: Amar",
"givenName: Aaccf",
"postalCode: 85762",
"userPassword: password",
"initials: AA");
// @formatter:on
}
/**
* Tests for issue 3891
* S1 S2
* ADD uid=xx,ou=parent,... [SUBTREE] DEL ou=parent, ...
*
* 1/ removeParentConflict1 (on S1)
* - t1(csn1) ADD uid=xx,ou=parent,...
* - t2(csn2) replay SUBTREE DEL ou=parent, ....
* => No conflict : expect the parent entry & subtree to be deleted
*
* 2/ removeParentConflict2 (on S1)
* - t1(csn1) ADD uid=xx,ou=parent,...
* - replay t2(csn2) DEL ou=parent, ....
* => Conflict and no automatic resolution: expect
* - the child entry to be renamed under root entry
* - the parent entry to be deleted
*
* 3/ removeParentConflict3 (on S2)
* - t2(csn2) DEL or SUBTREE DEL ou=parent, ....
* - t1(csn1) replay ADD uid=xx,ou=parent,...
* => Conflict and no automatic resolution: expect
* - the child entry to be renamed under root entry
*
*/
@Test
public void removeParentConflict1() throws Exception
{
Entry parentEntry = createParentEntry();
Entry childEntry = createChildEntry();
TestCaseUtils.addEntry(parentEntry);
TestCaseUtils.addEntry(childEntry);
String parentUUID = getEntryUUID(parentEntry.getName());
CSN csn2 = gen.newCSN();
DeleteMsg delMsg = new DeleteMsg(parentEntry.getName(), csn2, parentUUID);
delMsg.setSubtreeDelete(true);
replayMsg(delMsg);
// Expect the subtree to be deleted and no conflict entry created
assertFalse(entryExists(parentEntry.getName()), "DEL subtree on parent was not processed as expected.");
assertFalse(entryExists(parentEntry.getName()), "DEL subtree on parent was not processed as expected.");
}
@Test
public void removeParentConflict2() throws Exception
{
Entry parentEntry = createParentEntry();
Entry childEntry = createChildEntry();
TestCaseUtils.addEntry(parentEntry);
TestCaseUtils.addEntry(childEntry);
String parentUUID = getEntryUUID(parentEntry.getName());
String childUUID = getEntryUUID(childEntry.getName());
CSN csn2 = gen.newCSN();
DeleteMsg delMsg = new DeleteMsg(parentEntry.getName(), csn2, parentUUID);
// NOT SUBTREE
replayMsg(delMsg);
// Expect the parent entry to be deleted
assertFalse(entryExists(parentEntry.getName()), "Parent entry expected to be deleted : " + parentEntry.getName());
// Expect the child entry to be moved as conflict entry under the root
// entry of the suffix
DN childDN = DN.valueOf("entryuuid=" + childUUID + "+cn=child,o=test");
assertTrue(entryExists(childDN), "Child entry conflict exist with DN=" + childDN);
}
@Test
public void removeParentConflict3() throws Exception
{
Entry parentEntry = createParentEntry();
Entry childEntry = createChildEntry();
TestCaseUtils.addEntry(parentEntry);
String parentUUID = getEntryUUID(parentEntry.getName());
TestCaseUtils.deleteEntry(parentEntry);
CSN csn1 = gen.newCSN();
// Create and publish an update message to add the child entry.
String childUUID = "44444444-4444-4444-4444-444444444444";
AddMsg addMsg = new AddMsg(
csn1,
childEntry.getName(),
childUUID,
parentUUID,
childEntry.getObjectClassAttribute(),
childEntry.getAllAttributes(), null);
// Put the message in the replay queue
replayMsg(addMsg);
// Expect the parent entry to be deleted
assertFalse(entryExists(parentEntry.getName()), "Parent entry exists ");
// Expect the child entry to be moved as conflict entry under the root
// entry of the suffix
DN childDN = DN.valueOf("entryuuid=" + childUUID + "+cn=child,o=test");
assertTrue(entryExists(childDN), "Child entry conflict exist with DN=" + childDN);
}
private Entry createParentEntry() throws Exception
{
return TestCaseUtils.makeEntry(
"dn: ou=rpConflict, "+ TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: organizationalUnit");
}
private Entry createChildEntry() throws Exception
{
// @formatter:off
return TestCaseUtils.makeEntry(
"dn: cn=child, ou=rpConflict,"+ TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: user.1",
"description: This is the description for Aaccf Amar.",
"st: NC",
"postalAddress: Aaccf Amar$17984 Thirteenth Street$Rockford, NC 85762",
"mail: user.1@example.com",
"cn: Aaccf Amar",
"l: Rockford",
"street: 17984 Thirteenth Street",
"employeeNumber: 1",
"sn: Amar",
"givenName: Aaccf",
"postalCode: 85762",
"userPassword: password",
"initials: AA");
// @formatter:on
}
private void replayMsg(UpdateMsg updateMsg) throws InterruptedException
{
domain.processUpdate(updateMsg);
LDAPUpdateMsg ldapUpdate = queue.take().getUpdateMessage();
domain.markInProgress(ldapUpdate);
domain.replay(ldapUpdate, SHUTDOWN);
}
}