/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2013 ForgeRock AS
*/
package org.opends.server.replication.plugin;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.protocol.AddMsg;
import org.opends.server.replication.protocol.LDAPUpdateMsg;
import org.opends.server.replication.protocol.ModifyMsg;
import org.opends.server.replication.service.ReplicationBroker;
import org.opends.server.tools.LDAPModify;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.Attributes;
import org.opends.server.types.ByteString;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.Operation;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchScope;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
import static org.opends.server.TestCaseUtils.*;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
/**
* Tests the Historical class.
*/
public class HistoricalTest
extends ReplicationTestCase
{
private int replServerPort;
String testName = "historicalTest";
/**
* Set up replication on the test backend.
* @throws Exception If an error occurs.
*/
@BeforeClass
@Override
public void setUp()
throws Exception
{
super.setUp();
// Create an internal connection.
connection = InternalClientConnection.getRootConnection();
// find a free port for the replicationServer
ServerSocket socket = TestCaseUtils.bindFreePort();
replServerPort = socket.getLocalPort();
socket.close();
// The replication server.
String replServerStringDN = "cn=Replication Server, " + SYNCHRO_PLUGIN_DN;
String replServerLdif = "dn: " + replServerStringDN + "\n"
+ "objectClass: top\n"
+ "objectClass: ds-cfg-replication-server\n"
+ "cn: replication Server\n"
+ "ds-cfg-replication-port: " + replServerPort + "\n"
+ "ds-cfg-replication-db-directory: HistoricalTest\n"
+ "ds-cfg-replication-server-id: 102\n";
replServerEntry = TestCaseUtils.entryFromLdifString(replServerLdif);
// The suffix to be synchronized.
String synchroServerStringDN = "cn=" + testName + ", cn=domains, " +
SYNCHRO_PLUGIN_DN;
String synchroServerLdif = "dn: " + synchroServerStringDN + "\n"
+ "objectClass: top\n"
+ "objectClass: ds-cfg-replication-domain\n"
+ "cn: " + testName + "\n"
+ "ds-cfg-base-dn: " + TEST_ROOT_DN_STRING + "\n"
+ "ds-cfg-replication-server: localhost:" + replServerPort + "\n"
+ "ds-cfg-server-id: 1\n"
+ "ds-cfg-receive-status: true\n";
synchroServerEntry = TestCaseUtils.entryFromLdifString(synchroServerLdif);
configureReplication();
}
/**
* Tests that the attribute modification history is correctly read from
* and written to an operational attribute of the entry.
* Also test that historical is purged according to the purge delay that
* is provided.
* @throws Exception If the test fails.
*/
@Test(enabled=true)
public void testEncodingAndPurge()
throws Exception
{
// Add a test entry.
TestCaseUtils.addEntry(
"dn: uid=user.1," + TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: user.1",
"cn: Aaccf Amar",
"sn: Amar",
"givenName: Aaccf",
"userPassword: password",
"description: Initial description",
"displayName: 1"
);
// Modify the test entry to give it some history.
// Test both single and multi-valued attributes.
String path = TestCaseUtils.createTempFile(
"dn: uid=user.1," + TEST_ROOT_DN_STRING,
"changetype: modify",
"add: cn;lang-en",
"cn;lang-en: Aaccf Amar",
"cn;lang-en: Aaccf A Amar",
"-",
"replace: description",
"description: replaced description",
"-",
"add: displayName",
"displayName: 2",
"-",
"delete: displayName",
"displayName: 1",
"-"
);
String[] args =
{
"-h", "127.0.0.1",
"-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
"-D", "cn=Directory Manager",
"-w", "password",
"-f", path
};
assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
args[9] = TestCaseUtils.createTempFile(
"dn: uid=user.1," + TEST_ROOT_DN_STRING,
"changetype: modify",
"replace: displayName",
"displayName: 2",
"-"
);
assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
// Read the entry back to get its history operational attribute.
DN dn = DN.decode("uid=user.1," + TEST_ROOT_DN_STRING);
Entry entry = DirectoryServer.getEntry(dn);
List<Attribute> attrs = EntryHistorical.getHistoricalAttr(entry);
Attribute before = attrs.get(0);
// Check that encoding and decoding preserves the history information.
EntryHistorical hist = EntryHistorical.newInstanceFromEntry(entry);
Attribute after = hist.encodeAndPurge();
assertEquals(hist.getLastPurgedValuesCount(),0);
assertEquals(after, before);
LDAPReplicationDomain domain = MultimasterReplication.findDomain(
DN.decode("uid=user.1," + TEST_ROOT_DN_STRING), null);
Thread.sleep(1000);
args[9] = TestCaseUtils.createTempFile(
"dn: uid=user.1," + TEST_ROOT_DN_STRING,
"changetype: modify",
"replace: displayName",
"displayName: 3",
"-"
);
assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
long testPurgeDelayInMillisec = 1000; // 1 sec
// Read the entry back to get its history operational attribute.
entry = DirectoryServer.getEntry(dn);
hist = EntryHistorical.newInstanceFromEntry(entry);
hist.setPurgeDelay(testPurgeDelayInMillisec);
after = hist.encodeAndPurge();
// The purge time is not done so the hist attribute should be not empty
assertTrue(!after.isEmpty());
// Now wait for the purge time to be done
Thread.sleep(testPurgeDelayInMillisec + 200);
// Read the entry back to get its history operational attribute.
// The hist attribute should now be empty since purged
entry = DirectoryServer.getEntry(dn);
hist = EntryHistorical.newInstanceFromEntry(entry);
hist.setPurgeDelay(testPurgeDelayInMillisec);
after = hist.encodeAndPurge();
assertTrue(after.isEmpty());
assertEquals(hist.getLastPurgedValuesCount(),11);
}
/**
* The scenario for this test case is that two modify operations occur at
* two different servers at nearly the same time, each operation adding a
* different value for a single-valued attribute. Replication then
* replays the operations and we expect the conflict to be resolved on both
* servers by keeping whichever value was actually added first.
* For the unit test, we employ a single directory server. We use the
* broker API to simulate the ordering that would happen on the first server
* on one entry, and the reverse ordering that would happen on the
* second server on a different entry. Confused yet?
* @throws Exception If the test fails.
*/
@Test(enabled=true, groups="slow")
public void conflictSingleValue()
throws Exception
{
final DN dn1 = DN.decode("cn=test1," + TEST_ROOT_DN_STRING);
final DN dn2 = DN.decode("cn=test2," + TEST_ROOT_DN_STRING);
final DN baseDn = DN.decode(TEST_ROOT_DN_STRING);
final AttributeType attrType =
DirectoryServer.getAttributeType("displayname");
final AttributeType entryuuidType =
DirectoryServer.getAttributeType("entryuuid");
/*
* Open a session to the replicationServer using the broker API.
* This must use a different serverId to that of the directory server.
*/
ReplicationBroker broker =
openReplicationSession(baseDn, 2, 100, replServerPort, 1000, true);
// Clear the backend and create top entrye
TestCaseUtils.initializeTestBackend(true);
// Add the first test entry.
TestCaseUtils.addEntry(
"dn: cn=test1," + TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"cn: test1",
"sn: test"
);
// Read the entry back to get its UUID.
Entry entry = DirectoryServer.getEntry(dn1);
List<Attribute> attrs = entry.getAttribute(entryuuidType);
String entryuuid =
attrs.get(0).iterator().next().getValue().toString();
// Add the second test entry.
TestCaseUtils.addEntry(
"dn: cn=test2," + TEST_ROOT_DN_STRING,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"cn: test2",
"sn: test",
"description: Description"
);
// Read the entry back to get its UUID.
entry = DirectoryServer.getEntry(dn2);
attrs = entry.getAttribute(entryuuidType);
String entryuuid2 =
attrs.get(0).iterator().next().getValue().toString();
long now = System.currentTimeMillis();
// A change on a first server.
ChangeNumber t1 = new ChangeNumber(now, 0, 3);
// A change on a second server.
ChangeNumber t2 = new ChangeNumber(now+1, 0, 4);
// Simulate the ordering t1:add:A followed by t2:add:B that would
// happen on one server.
// Replay an add of a value A at time t1 on a first server.
Attribute attr = Attributes.create(attrType.getNormalizedPrimaryName(), "A");
Modification mod = new Modification(ModificationType.ADD, attr);
publishModify(broker, t1, dn1, entryuuid, mod);
// It would be nice to avoid these sleeps.
// We need to preserve the replay order but the order could be changed
// due to the multi-threaded nature of the replication replay.
// Putting a sentinel value in the modification is not foolproof since
// the operation might not get replayed at all.
Thread.sleep(2000);
// Replay an add of a value B at time t2 on a second server.
attr = Attributes.create(attrType.getNormalizedPrimaryName(), "B");
mod = new Modification(ModificationType.ADD, attr);
publishModify(broker, t2, dn1, entryuuid, mod);
// Simulate the reverse ordering t2:add:B followed by t1:add:A that
// would happen on the other server.
t1 = new ChangeNumber(now+3, 0, 3);
t2 = new ChangeNumber(now+4, 0, 4);
// Replay an add of a value B at time t2 on a second server.
attr = Attributes.create(attrType.getNormalizedPrimaryName(), "B");
mod = new Modification(ModificationType.ADD, attr);
publishModify(broker, t2, dn2, entryuuid2, mod);
Thread.sleep(2000);
// Replay an add of a value A at time t1 on a first server.
attr = Attributes.create(attrType.getNormalizedPrimaryName(), "A");
mod = new Modification(ModificationType.ADD, attr);
publishModify(broker, t1, dn2, entryuuid2, mod);
Thread.sleep(2000);
// Read the first entry to see how the conflict was resolved.
entry = DirectoryServer.getEntry(dn1);
attrs = entry.getAttribute(attrType);
String attrValue1 =
attrs.get(0).iterator().next().getValue().toString();
// Read the second entry to see how the conflict was resolved.
entry = DirectoryServer.getEntry(dn2);
attrs = entry.getAttribute(attrType);
String attrValue2 =
attrs.get(0).iterator().next().getValue().toString();
// The two values should be the first value added.
assertEquals(attrValue1, "A");
assertEquals(attrValue2, "A");
TestCaseUtils.deleteEntry(DN.decode("cn=test1," + TEST_ROOT_DN_STRING));
TestCaseUtils.deleteEntry(DN.decode("cn=test2," + TEST_ROOT_DN_STRING));
}
private static
void publishModify(ReplicationBroker broker, ChangeNumber changeNum,
DN dn, String entryuuid, Modification mod)
{
List<Modification> mods = new ArrayList<Modification>(1);
mods.add(mod);
ModifyMsg modMsg = new ModifyMsg(changeNum, dn, mods, entryuuid);
broker.publish(modMsg);
}
/**
* Test that historical information is correctly added when performaing ADD,
* MOD and MODDN operations.
*/
@Test()
public void historicalAdd() throws Exception
{
final DN dn1 = DN.decode("cn=testHistoricalAdd,o=test");
// Clear the backend.
TestCaseUtils.initializeTestBackend(true);
// Add the first test entry.
TestCaseUtils.addEntry(
"dn: " + dn1,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"cn: test1",
"sn: test"
);
// Read the entry that was just added.
Entry entry = DirectoryServer.getEntry(dn1);
// Check that we can build an Add operation from this entry.
// This will ensure both that the Add historical information is
// correctly added and also that the code that rebuild operation
// from this historical information is working.
Iterable<FakeOperation> ops = EntryHistorical.generateFakeOperations(entry);
// Perform a few check on the Operation to see that it
// was correctly generated.
assertFakeOperations(dn1, entry, ops, 1);
// Now apply a modifications to the entry and check that the
// ADD historical information has been preserved.
TestCaseUtils.applyModifications(false,
"dn: " + dn1,
"changetype: modify",
"add: description",
"description: foo");
// Read the modified entry.
entry = DirectoryServer.getEntry(dn1);
// use historical information to generate new list of operations
// equivalent to the operations that have been applied to this entry.
ops = EntryHistorical.generateFakeOperations(entry);
// Perform a few check on the operation list to see that it
// was correctly generated.
assertFakeOperations(dn1, entry, ops, 2);
// rename the entry.
TestCaseUtils.applyModifications(false,
"dn: " + dn1,
"changetype: moddn",
"newrdn: cn=test2",
"deleteoldrdn: 1");
// Read the modified entry.
final DN dn2 = DN.decode("cn=test2,o=test");
entry = DirectoryServer.getEntry(dn2);
// use historical information to generate new list of operations
// equivalent to the operations that have been applied to this entry.
ops = EntryHistorical.generateFakeOperations(entry);
// Perform a few check on the operation list to see that it
// was correctly generated.
assertFakeOperations(dn2, entry, ops, 3);
// Now clear the backend and try to run the generated operations
// to check that applying them do lead to an equivalent result.
TestCaseUtils.initializeTestBackend(true);
for (FakeOperation fake : ops)
{
LDAPUpdateMsg msg = (LDAPUpdateMsg) fake.generateMessage();
Operation op =
msg.createOperation(InternalClientConnection.getRootConnection());
op.setInternalOperation(true);
op.setSynchronizationOperation(true);
op.run();
}
Entry newEntry = DirectoryServer.getEntry(dn2);
assertEquals(entry.getDN(), newEntry.getDN());
}
/**
* Performs a few check on the provided ADD operations, particularly
* that a ADDmsg can be created from it with valid values for fields
* DN, entryuid, ...)
*/
private void assertFakeOperations(final DN dn1, Entry entry,
Iterable<FakeOperation> ops, int assertCount) throws Exception
{
int count = 0;
for (FakeOperation op : ops)
{
count++;
if (op instanceof FakeAddOperation)
{
// perform a few check on the Operation to see that it
// was correctly generated :
// - the dn should be dn1,
// - the entry id and the parent id should match the ids from the entry
FakeAddOperation addOp = (FakeAddOperation) op;
assertTrue(addOp.getChangeNumber() != null);
AddMsg addmsg = addOp.generateMessage();
assertTrue(dn1.equals(DN.decode(addmsg.getDn())));
assertTrue(addmsg.getEntryUUID().equals(EntryHistorical.getEntryUUID(entry)));
String parentId = LDAPReplicationDomain.findEntryUUID(dn1.getParent());
assertTrue(addmsg.getParentEntryUUID().equals(parentId));
addmsg.createOperation(InternalClientConnection.getRootConnection());
} else
{
if (count == 1)
{
// The first operation should be an ADD operation.
fail("FakeAddOperation was not correctly generated"
+ " from historical information");
}
}
}
assertEquals(count, assertCount);
}
/**
* Test the task that purges the replication historical stored in the user
* entry.
* Steps :
* - creates entry containing historical
* - wait for the pruge delay
* - lauch the purge task
* - verify that all historical has been purged
*
* TODO: another test should be written that configures the task no NOT have
* the time to purge everything in 1 run .. and thus to relauch it to finish
* the purge. And verify that the second run starts on the changeNumber where
* the previous task run had stopped.
*
* @throws Exception If the test fails.
*/
@Test(enabled=true)
public void testRecurringPurgeIn1Run()
throws Exception
{
int entryCnt = 10;
addEntriesWithHistorical(1, entryCnt);
// set the purge delay to 1 sec
TestCaseUtils.dsconfig(
"set-replication-domain-prop",
"--provider-name","Multimaster Synchronization",
"--domain-name",testName,
"--set","conflicts-historical-purge-delay:1m");
Thread.sleep(60*1000);
// launch the purge
Entry taskInit = TestCaseUtils.makeEntry(
"dn: ds-task-id=" + UUID.randomUUID() +
",cn=Scheduled Tasks,cn=Tasks",
"objectclass: top",
"objectclass: ds-task",
"objectclass: ds-task-purge-conflicts-historical",
"ds-task-class-name: org.opends.server.tasks.PurgeConflictsHistoricalTask",
"ds-task-purge-conflicts-historical-domain-dn: "+TEST_ROOT_DN_STRING,
"ds-task-purge-conflicts-historical-maximum-duration: 120"); // 120 sec
addTask(taskInit, ResultCode.SUCCESS, null);
// every entry should be purged from its hist
try
{
// Search for matching entries in config backend
InternalSearchOperation op = connection.processSearch(
ByteString.valueOf(TEST_ROOT_DN_STRING),
SearchScope.WHOLE_SUBTREE,
LDAPFilter.decode("(ds-sync-hist=*)"));
assertEquals(op.getResultCode(), ResultCode.SUCCESS,
op.getErrorMessage().toString());
// Check that no entries have been found
LinkedList<SearchResultEntry> entries = op.getSearchEntries();
assertTrue(entries != null);
assertEquals(entries.size(), 0);
} catch (Exception e)
{
fail("assertNoConfigEntriesWithFilter: could not search config backend" + e.getMessage());
}
}
/**
* Add a provided number of generated entries containing historical.
* @param dnSuffix A suffix to be added to the dn
* @param entryCnt The number of entries to create
* @throws Exception
*/
private void addEntriesWithHistorical(int dnSuffix, int entryCnt)
throws Exception
{
for (int i=0; i<entryCnt;i++)
{
String sdn = "dn: uid=user"+i+dnSuffix+"," + TEST_ROOT_DN_STRING;
// Add a test entry.
TestCaseUtils.addEntry(
sdn,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: user"+i,
"cn: Aaccf Amar",
"sn: Amar",
"givenName: Aaccf",
"userPassword: password",
"description: Initial description",
"displayName: 1"
);
// Modify the test entry to give it some history.
// Test both single and multi-valued attributes.
String path = TestCaseUtils.createTempFile(
sdn,
"changetype: modify",
"add: cn;lang-en",
"cn;lang-en: Aaccf Amar",
"cn;lang-en: Aaccf A Amar",
"-",
"replace: givenName",
"givenName: new given",
"-",
"replace: userPassword",
"userPassword: new pass",
"-",
"replace: description",
"description: replaced description",
"-",
"replace: sn",
"sn: replaced sn",
"-",
"add: displayName",
"displayName: 2",
"-",
"delete: displayName",
"displayName: 1",
"-"
);
String[] args =
{
"-h", "127.0.0.1",
"-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
"-D", "cn=Directory Manager",
"-w", "password",
"-f", path
};
assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
args[9] = TestCaseUtils.createTempFile(
sdn,
"changetype: modify",
"replace: displayName",
"displayName: 2",
"-"
);
assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
}
}
}