opends/resource/config/config.ldif
@@ -223,6 +223,12 @@ ds-cfg-attribute: ds-sync-hist ds-cfg-index-type: ordering dn: ds-cfg-attribute=ds-sync-conflict,cn=Index,ds-cfg-backend-id=userRoot,cn=Backends,cn=config objectClass: top objectClass: ds-cfg-local-db-index ds-cfg-attribute: ds-sync-conflict ds-cfg-index-type: equality dn: ds-cfg-attribute=entryUUID,cn=Index,ds-cfg-backend-id=userRoot,cn=Backends,cn=config objectClass: top objectClass: ds-cfg-local-db-index opends/resource/schema/02-config.ldif
@@ -1552,7 +1552,8 @@ X-ORIGIN 'OpenDS Directory Server' ) attributeTypes: ( 1.3.6.1.4.1.26027.1.1.317 NAME 'ds-sync-conflict' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE directoryOperation X-ORIGIN 'OpenDS Directory Server' ) attributeTypes: ( 1.3.6.1.4.1.26027.1.1.319 opends/src/admin/defn/org/opends/server/admin/std/LocalDBBackendConfiguration.xml
@@ -84,6 +84,14 @@ <adm:value>ds-sync-hist</adm:value> </adm:property> </adm:default-managed-object> <adm:default-managed-object name="ds-sync-conflict"> <adm:property name="index-type"> <adm:value>equality</adm:value> </adm:property> <adm:property name="attribute"> <adm:value>ds-sync-conflict</adm:value> </adm:property> </adm:default-managed-object> </adm:one-to-many> <adm:profile name="ldap"> <ldap:rdn-sequence>cn=Index</ldap:rdn-sequence> opends/src/messages/messages/replication.properties
@@ -495,3 +495,5 @@ translate RUV into state for suffix %s SEVERE_ERR_RSQUEUE_DIFFERENT_MSGS_WITH_SAME_CN_201=Processing two different \ changes with same changeNumber=%s. Previous msg=<%s>, New msg=<%s> SEVERE_ERR_COULD_NOT_SOLVE_CONFLICT_202=Error while trying to solve conflict \ with DN : %s ERROR : %s opends/src/server/org/opends/server/replication/plugin/Historical.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2009 Sun Microsystems, Inc. * Copyright 2006-2010 Sun Microsystems, Inc. */ package org.opends.server.replication.plugin; @@ -449,6 +449,25 @@ /** * Returns the lastChangeNumber when the entry DN was modified. * * @return The lastChangeNumber when the entry DN was modified. */ public ChangeNumber getDNDate() { if (ADDDate == null) return MODDNDate; if (MODDNDate == null) return ADDDate; if (MODDNDate.older(ADDDate)) return MODDNDate; else return ADDDate; } /** * read the historical information from the entry attribute and * load it into the Historical object attached to the entry. * @param entry The entry which historical information must be loaded opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -2411,6 +2411,148 @@ pendingChanges.pushCommittedChanges(); } } checkForClearedConflict(op); } /** * Check if the operation that just happened has cleared a conflict : * Clearing a conflict happens if the operation has free a DN that * for which an other entry was in conflict. */ private void checkForClearedConflict(PostOperationOperation op) { OperationType type = op.getOperationType(); if (op.getResultCode() != ResultCode.SUCCESS) { // those operations cannot have cleared a conflict return; } DN targetDN; if (type == OperationType.DELETE) { targetDN = ((PostOperationDeleteOperation) op).getEntryDN(); } else if (type == OperationType.MODIFY_DN) { targetDN = ((PostOperationModifyDNOperation) op).getEntryDN(); } else { return; } LDAPFilter filter = null; try { filter = LDAPFilter.decode( DS_SYNC_CONFLICT + "=" + targetDN.toNormalizedString()); } catch (LDAPException e) { // Not possible. We know the filter just above is correct. } LinkedHashSet<String> attrs = new LinkedHashSet<String>(1); attrs.add(Historical.HISTORICALATTRIBUTENAME); attrs.add(Historical.ENTRYUIDNAME); attrs.add("*"); InternalSearchOperation searchOp = conn.processSearch( ByteString.valueOf(baseDn.toString()), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false, filter, attrs, null); LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries(); Entry entrytoRename = null; ChangeNumber entrytoRenameDate = null; for (SearchResultEntry entry : entries) { Historical history = Historical.load(entry); if (entrytoRename == null) { entrytoRename = entry; entrytoRenameDate = history.getDNDate(); } else if (!history.AddedOrRenamedAfter(entrytoRenameDate)) { // this conflict is older than the previous, keep it. entrytoRename = entry; entrytoRenameDate = history.getDNDate(); } } if (entrytoRename != null) { DN entryDN = entrytoRename.getDN(); ModifyDNOperationBasis newOp = renameEntry( entryDN, targetDN.getRDN(), targetDN.getParent(), false); ResultCode res = newOp.getResultCode(); if (res != ResultCode.SUCCESS) { Message message = ERR_COULD_NOT_SOLVE_CONFLICT.get(entryDN.toString(), res.toString()); logError(message); } } } /** * Rename an Entry Using a synchronization, non-replicated operation. * This method should be used instead of the InternalConnection methods * when the operation that need to be run must be local only and therefore * not replicated to the RS. * * @param targetDN The DN of the entry to rename. * @param newRDN The new RDN to be used. * @param parentDN The parentDN to be used. * @param markConflict A boolean indicating is this entry should be marked * as a conflicting entry. In such case the * DS_SYNC_CONFLICT attribute will be added to the entry * with the value of its original DN. * If false, the DS_SYNC_CONFLICT attribute will be * cleared. * * @return The operation that was run to rename the entry. */ private ModifyDNOperationBasis renameEntry( DN targetDN, RDN newRDN, DN parentDN, boolean markConflict) { InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperationBasis newOp = new ModifyDNOperationBasis( conn, InternalClientConnection.nextOperationID(), InternalClientConnection.nextMessageID(), new ArrayList<Control>(0), targetDN, newRDN, false, parentDN); newOp.setInternalOperation(true); newOp.setSynchronizationOperation(true); newOp.setDontSynchronize(true); if (markConflict) { AttributeType attrType = DirectoryServer.getAttributeType(DS_SYNC_CONFLICT, true); Attribute attr = Attributes.create(attrType, AttributeValues.create( attrType, targetDN.toString())); Modification mod = new Modification(ModificationType.REPLACE, attr); newOp.addModification(mod); } else { AttributeType attrType = DirectoryServer.getAttributeType(DS_SYNC_CONFLICT, true); Attribute attr = Attributes.empty(attrType); Modification mod = new Modification(ModificationType.DELETE, attr); newOp.addModification(mod); } newOp.run(); return newOp; } /** @@ -3188,7 +3330,6 @@ * and keep the entry as a conflicting entry, */ conflict = true; markConflictEntry(conflictOp, entry.getDN(), entryDN); renameConflictEntry(conflictOp, entry.getDN(), Historical.getEntryUuid(entry)); } @@ -3233,11 +3374,8 @@ */ private void renameConflictEntry(Operation conflictOp, DN dn, String uid) { InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation newOp = conn.processModifyDN( dn, generateDeleteConflictDn(uid, dn),false, baseDn); ModifyDNOperation newOp = renameEntry(dn, generateDeleteConflictDn(uid, dn), baseDn, true); if (newOp.getResultCode() != ResultCode.SUCCESS) { @@ -3275,7 +3413,18 @@ List<Modification> mods = new ArrayList<Modification>(); Modification mod = new Modification(ModificationType.REPLACE, attr); mods.add(mod); ModifyOperation newOp = conn.processModify(currentDN, mods); ModifyOperationBasis newOp = new ModifyOperationBasis( conn, InternalClientConnection.nextOperationID(), InternalClientConnection.nextMessageID(), new ArrayList<Control>(0), currentDN, mods); newOp.setInternalOperation(true); newOp.setSynchronizationOperation(true); newOp.setDontSynchronize(true); newOp.run(); if (newOp.getResultCode() != ResultCode.SUCCESS) { // Log information for the repair tool. opends/tests/unit-tests-testng/src/server/org/opends/server/replication/plugin/NamingConflictTest.java
@@ -34,6 +34,8 @@ import org.opends.server.TestCaseUtils; import org.opends.server.admin.std.meta.ReplicationDomainCfgDefn.IsolationPolicy; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.replication.ReplicationTestCase; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.ChangeNumberGenerator; @@ -42,9 +44,12 @@ import org.opends.server.replication.protocol.DeleteMsg; import org.opends.server.types.Attribute; import org.opends.server.types.DN; import org.opends.server.types.RDN; import org.opends.server.types.Entry; import org.opends.server.types.ResultCode; import org.testng.annotations.Test; import static org.testng.Assert.*; @@ -149,6 +154,181 @@ } /** * 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. * * @throws Exception if the test fails. */ @Test(enabled=true) public void conflictCleaningDelete() throws Exception { TestCaseUtils.initializeTestBackend(true); final DN baseDn = DN.decode(TEST_ROOT_DN_STRING); TestSynchronousReplayQueue queue = new TestSynchronousReplayQueue(); DomainFakeCfg conf = new DomainFakeCfg(baseDn, 1, new TreeSet<String>()); conf.setIsolationPolicy(IsolationPolicy.ACCEPT_ALL_UPDATES); LDAPReplicationDomain domain = MultimasterReplication.createNewDomain(conf, queue); domain.start(); try { /* * Create a Change number generator to generate new ChangeNumbers * when we need to send operations messages to the replicationServer. */ ChangeNumberGenerator gen = new ChangeNumberGenerator(201, 0); String entryldif = "dn: cn=conflictCleaningDelete, "+ TEST_ROOT_DN_STRING + "\n" + "objectClass: top\n" + "objectClass: person\n" + "objectClass: organizationalPerson\n" + "objectClass: inetOrgPerson\n" + "uid: user.1\n" + "description: This is the description for Aaccf Amar.\n" + "st: NC\n" + "postalAddress: Aaccf Amar$17984 Thirteenth Street" + "$Rockford, NC 85762\n" + "mail: user.1@example.com\n" + "cn: Aaccf Amar\n" + "l: Rockford\n" + "street: 17984 Thirteenth Street\n" + "employeeNumber: 1\n" + "sn: Amar\n" + "givenName: Aaccf\n" + "postalCode: 85762\n" + "userPassword: password\n" + "initials: AA\n"; Entry entry = TestCaseUtils.entryFromLdifString(entryldif); // Add the first entry TestCaseUtils.addEntry(entry); String parentUUID = getEntryUUID(DN.decode(TEST_ROOT_DN_STRING)); ChangeNumber cn1 = gen.newChangeNumber(); // Now try to add the same entry with same DN but a different // unique ID though the replication AddMsg addMsg = new AddMsg(cn1, entry.getDN().toNormalizedString(), "c9cb8c3c-615a-4122-865d-50323aaaed48", parentUUID, entry.getObjectClasses(), entry.getUserAttributes(), null); // Put the message in the replay queue domain.processUpdate(addMsg); // Make the domain replay the change from the replay queue domain.replay(queue.take().getUpdateMessage()); // Now delete the first entry that was added at the beginning TestCaseUtils.deleteEntry(entry.getDN()); // Expect the conflict resolution : the second entry should now // have been renamed with the original DN. Entry resultEntry = DirectoryServer.getEntry(entry.getDN()); assertTrue(resultEntry != null, "The conflict was not cleared"); assertEquals(getEntryUUID(resultEntry.getDN()), "c9cb8c3c-615a-4122-865d-50323aaaed48", "The wrong entry has been renamed"); assertNull(resultEntry.getAttribute(LDAPReplicationDomain.DS_SYNC_CONFLICT)); } finally { MultimasterReplication.deleteDomain(baseDn); } } /** * 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. * * @throws Exception if the test fails. */ @Test(enabled=true) public void conflictCleaningMODDN() throws Exception { TestCaseUtils.initializeTestBackend(true); final DN baseDn = DN.decode(TEST_ROOT_DN_STRING); TestSynchronousReplayQueue queue = new TestSynchronousReplayQueue(); DomainFakeCfg conf = new DomainFakeCfg(baseDn, 1, new TreeSet<String>()); conf.setIsolationPolicy(IsolationPolicy.ACCEPT_ALL_UPDATES); LDAPReplicationDomain domain = MultimasterReplication.createNewDomain(conf, queue); domain.start(); try { /* * Create a Change number generator to generate new ChangeNumbers * when we need to send operations messages to the replicationServer. */ ChangeNumberGenerator gen = new ChangeNumberGenerator(201, 0); String entryldif = "dn: cn=conflictCleaningDelete, "+ TEST_ROOT_DN_STRING + "\n" + "objectClass: top\n" + "objectClass: person\n" + "objectClass: organizationalPerson\n" + "objectClass: inetOrgPerson\n" + "uid: user.1\n" + "description: This is the description for Aaccf Amar.\n" + "st: NC\n" + "postalAddress: Aaccf Amar$17984 Thirteenth Street" + "$Rockford, NC 85762\n" + "mail: user.1@example.com\n" + "cn: Aaccf Amar\n" + "l: Rockford\n" + "street: 17984 Thirteenth Street\n" + "employeeNumber: 1\n" + "sn: Amar\n" + "givenName: Aaccf\n" + "postalCode: 85762\n" + "userPassword: password\n" + "initials: AA\n"; Entry entry = TestCaseUtils.entryFromLdifString(entryldif); // Add the first entry TestCaseUtils.addEntry(entry); String parentUUID = getEntryUUID(DN.decode(TEST_ROOT_DN_STRING)); ChangeNumber cn1 = gen.newChangeNumber(); // Now try to add the same entry with same DN but a different // unique ID though the replication AddMsg addMsg = new AddMsg(cn1, entry.getDN().toNormalizedString(), "c9cb8c3c-615a-4122-865d-50323aaaed48", parentUUID, entry.getObjectClasses(), entry.getUserAttributes(), null); // Put the message in the replay queue domain.processUpdate(addMsg); // Make the domain replay the change from the replay queue domain.replay(queue.take().getUpdateMessage()); // Now delete the first entry that was added at the beginning InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation modDNOperation = conn.processModifyDN(entry.getDN(), RDN.decode("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.getDN()); assertTrue(resultEntry != null, "The conflict was not cleared"); assertEquals(getEntryUUID(resultEntry.getDN()), "c9cb8c3c-615a-4122-865d-50323aaaed48", "The wrong entry has been renamed"); assertNull(resultEntry.getAttribute(LDAPReplicationDomain.DS_SYNC_CONFLICT)); } finally { MultimasterReplication.deleteDomain(baseDn); } } /** * Tests for issue 3891 * S1 S2 * ADD uid=xx,ou=parent,... [SUBTREE] DEL ou=parent, ... @@ -219,7 +399,6 @@ TestCaseUtils.addEntry(childEntry); String parentUUID = getEntryUUID(parentEntry.getDN()); String childUUID = getEntryUUID(childEntry.getDN()); ChangeNumber cn2 = gen.newChangeNumber();