/*
* Mojito Distributed Hash Table (Mojito DHT)
* Copyright (C) 2006-2007 LimeWire LLC
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.limewire.mojito;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import junit.framework.TestSuite;
import org.limewire.collection.PatriciaTrie;
import org.limewire.collection.Trie;
import org.limewire.collection.TrieUtils;
import org.limewire.mojito.concurrent.CallableDHTTask;
import org.limewire.mojito.concurrent.DHTFuture;
import org.limewire.mojito.concurrent.DHTTask;
import org.limewire.mojito.db.DHTValue;
import org.limewire.mojito.db.DHTValueEntity;
import org.limewire.mojito.db.DHTValueType;
import org.limewire.mojito.db.impl.DHTValueImpl;
import org.limewire.mojito.exceptions.DHTException;
import org.limewire.mojito.result.Result;
import org.limewire.mojito.result.StoreResult;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.Version;
import org.limewire.mojito.settings.BucketRefresherSettings;
import org.limewire.mojito.settings.DatabaseSettings;
import org.limewire.mojito.settings.KademliaSettings;
import org.limewire.mojito.settings.RouteTableSettings;
import org.limewire.mojito.util.UnitTestUtils;
import org.limewire.security.SecurityToken;
import org.limewire.util.StringUtils;
@SuppressWarnings("null")
public class CacheForwardTest extends MojitoTestCase {
private static final int PORT = 3000;
/*static {
System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog");
}*/
public CacheForwardTest(String name) {
super(name);
}
public static TestSuite suite() {
return buildTestSuite(CacheForwardTest.class);
}
public static void main(String[] args) throws Exception {
junit.textui.TestRunner.run(suite());
}
@Override
protected void setUp() throws Exception {
super.setUp();
setLocalIsPrivate(false);
RouteTableSettings.INCOMING_REQUESTS_UNKNOWN.setValue(true);
}
@SuppressWarnings("unchecked")
public void testGetSecurityToken() throws Exception {
MojitoDHT dht1 = null;
MojitoDHT dht2 = null;
try {
// Setup the first instance so that it thinks it's bootstrapping
dht1 = MojitoFactory.createDHT();
dht1.bind(2000);
dht1.start();
Context context1 = (Context)dht1;
UnitTestUtils.setBootstrapping(dht1, true);
assertFalse(dht1.isBootstrapped());
assertTrue(context1.isBootstrapping());
// And setup the second instance so that it thinks it's bootstrapped
dht2 = MojitoFactory.createDHT();
dht2.bind(3000);
dht2.start();
Context context2 = (Context)dht2;
UnitTestUtils.setBootstrapped(dht2, true);
assertTrue(dht2.isBootstrapped());
assertFalse(context2.isBootstrapping());
// Get the SecurityToken...
Class clazz = Class.forName("org.limewire.mojito.manager.StoreProcess$GetSecurityTokenHandler");
Constructor<DHTTask<Result>> con
= clazz.getDeclaredConstructor(Context.class, Contact.class);
con.setAccessible(true);
DHTTask<Result> task = con.newInstance(context2, context1.getLocalNode());
CallableDHTTask<Result> callable = new CallableDHTTask<Result>(task);
try {
Result result = callable.call();
clazz = Class.forName("org.limewire.mojito.manager.StoreProcess$GetSecurityTokenResult");
Method m = clazz.getDeclaredMethod("getSecurityToken", new Class[0]);
m.setAccessible(true);
SecurityToken securityToken = (SecurityToken)m.invoke(result, new Object[0]);
assertNotNull(securityToken);
} catch (ExecutionException err) {
assertInstanceof(DHTException.class, err.getCause());
fail("DHT-1 did not return a SecurityToken", err);
}
} finally {
if (dht1 != null) { dht1.close(); }
if (dht2 != null) { dht2.close(); }
}
}
public void disabledtestCacheForward() throws Exception {
// This test is testing a disabled feature and keeps failing.
// We disable this test for now. See LWC-2778 for detail
final long waitForNodes = 1000; // ms
final long BUCKET_REFRESH = 1 * 1000;
// it takes my machine 8.5 seconds to bootstrap the 3*k nodes and the build machine
// is faster than mine. So 8.5 seconds should be enough for the build machine to
// bootstrapp the 3*k nodes. To be safe, we set BOOTSTRAP_TIME to be 10 seconds.
// We use this value as BUCKET_REFRESHER_DELAY since we want all nodes finish
// bootstrapping (joining the network) before pinging nearest neighbors.
final long BOOTSTRAP_TIME = 10 * 1000;
//ContextSettings.SEND_SHUTDOWN_MESSAGE.setValue(false);
BucketRefresherSettings.BUCKET_REFRESHER_PING_NEAREST.setValue(BUCKET_REFRESH);
BucketRefresherSettings.BUCKET_REFRESHER_DELAY.setValue(BOOTSTRAP_TIME);
DatabaseSettings.DELETE_VALUE_IF_FURTHEST_NODE.setValue(false);
int k = KademliaSettings.REPLICATION_PARAMETER.getValue();
// KUID valueId = KUID.create("40229239B68FFA66575E59D0AB1F685AD3191960");
KUID valueId = KUID.createRandomID();
Map<KUID, MojitoDHT> dhts = new HashMap<KUID, MojitoDHT>();
MojitoDHT first = null;
try {
for (int i = 0; i < 3*k; i++) {
MojitoDHT dht = MojitoFactory.createDHT("DHT-" + i);
dht.bind(PORT + i);
dht.start();
if (i > 0) {
Thread.sleep(100);
dht.bootstrap(new InetSocketAddress("localhost", PORT)).get();
// the bootstrapper needs to ping every joining node.
first.ping(new InetSocketAddress("localhost", PORT+i)).get();
} else {
first = dht;
}
dhts.put(dht.getLocalNodeID(), dht);
}
first.bootstrap(new InetSocketAddress("localhost", PORT+1)).get();
Thread.sleep(waitForNodes);
// Sort all KUIDs by XOR distance and use the Node as
// creator that's furthest away from the value ID so
// that it never cannot be member of the k-closest Nodes
Trie<KUID, KUID> trie = new PatriciaTrie<KUID, KUID>(KUID.KEY_ANALYZER);
for (KUID id : dhts.keySet()) {
trie.put(id, id);
}
List<KUID> idsByXorDistance = TrieUtils.select(trie, valueId, trie.size());
// Creator happens to be the furthest of the k-closest Nodes.
//MojitoDHT creator = dhts.get(idsByXorDistance.get(k-1));
// Use the furthest Node as the creator.
MojitoDHT creator = dhts.get(idsByXorDistance.get(idsByXorDistance.size()-1));
// Store the value
DHTValue value = new DHTValueImpl(DHTValueType.TEST, Version.ZERO, StringUtils.toUTF8Bytes("Hello World"));
StoreResult evt = creator.put(valueId, value).get();
// see LWC-2778. In case the root is UNKNOWN in others route table,
// we wait BOOTSTRAP_TIME (the delay of ping the nearest bucket)
// so that every node has a chance to ping the root, hence realize
// the root is ALIVE
boolean waiting = true;
KUID rootsID = TrieUtils.select(trie, valueId, 1).get(0);
for (Contact c : evt.getLocations()){
if (c.getNodeID().equals(rootsID)){
waiting = false;
break;
}
}
if (waiting) {
for (Contact c: evt.getLocations()) {
Context dht = (Context)dhts.get(c.getNodeID());
dht.getDatabase().clear();
}
Thread.sleep(BOOTSTRAP_TIME);
evt = creator.put(valueId, value).get();
}
assertEquals(k, evt.getLocations().size());
// Give everybody time to process the store request
Thread.sleep(waitForNodes);
// And check the initial state
Context closest = null;
for (Contact remote : evt.getLocations()) {
Context dht = (Context)dhts.get(remote.getNodeID());
assertEquals(1, dht.getDatabase().getKeyCount());
assertEquals(1, dht.getDatabase().getValueCount());
for (DHTValueEntity dhtValue : dht.getDatabase().values()) {
assertEquals(valueId, dhtValue.getPrimaryKey());
assertEquals(value, dhtValue.getValue());
assertEquals(creator.getLocalNodeID(), dhtValue.getSecondaryKey());
assertEquals(creator.getLocalNodeID(), dhtValue.getSender().getNodeID());
}
if (closest == null) {
closest = dht;
}
}
// Create a Node with the nearest possible Node ID
// That means we set the Node ID to the Value ID
Context nearest = (Context)MojitoFactory.createDHT("Nearest");
Method m = nearest.getClass().getDeclaredMethod("setLocalNodeID", new Class[]{KUID.class});
m.setAccessible(true);
m.invoke(nearest, new Object[]{valueId});
nearest.bind(PORT+500);
nearest.start();
bootstrap(nearest, dhts.values());
// Give everybody time to figure out whether to forward
// a value or to remove it
Thread.sleep(waitForNodes);
// The Node with the nearest possible ID should have the value
assertEquals(1, nearest.getDatabase().getValueCount());
for (Contact remote : evt.getLocations()) {
Context dht = (Context)dhts.get(remote.getNodeID());
assertEquals("I dont have it?? "+dht.getLocalNodeID(),1, dht.getDatabase().getKeyCount());
assertEquals(1, dht.getDatabase().getValueCount());
for (DHTValueEntity dhtValue : dht.getDatabase().values()) {
assertEquals(valueId, dhtValue.getPrimaryKey());
assertEquals(value, dhtValue.getValue());
assertEquals(creator.getLocalNodeID(), dhtValue.getSecondaryKey());
assertEquals(creator.getLocalNodeID(), dhtValue.getSender().getNodeID());
}
}
// The 'nearest' Node received the value from the
// previous 'closest' Node
assertEquals(1, nearest.getDatabase().getKeyCount());
assertEquals(1, nearest.getDatabase().getValueCount());
for (DHTValueEntity dhtValue : nearest.getDatabase().values()) {
assertEquals(valueId, dhtValue.getPrimaryKey());
assertEquals(value, dhtValue.getValue());
assertEquals(creator.getLocalNodeID(), dhtValue.getSecondaryKey());
// The closest Node send us the value!
assertEquals(closest.getLocalNodeID(), dhtValue.getSender().getNodeID());
}
// Clear the Database but don't change the instanceId!
// The other Nodes don't know that we cleared our DB
// and will this not store values!
nearest.getDatabase().clear();
assertEquals(0, nearest.getDatabase().getKeyCount());
assertEquals(0, nearest.getDatabase().getValueCount());
bootstrap(nearest, dhts.values());
// Give everybody time to figure out whether to forward
// a value or to remove it
Thread.sleep(waitForNodes);
assertEquals(0, nearest.getDatabase().getKeyCount());
assertEquals(0, nearest.getDatabase().getValueCount());
// Change the instanceId and we'll asked to store the
// value again!
nearest.getLocalNode().nextInstanceID();
bootstrap(nearest, dhts.values());
// Give everybody time to figure out whether to forward
// a value or to remove it
Thread.sleep(waitForNodes);
assertEquals(1, nearest.getDatabase().getKeyCount());
assertEquals(1, nearest.getDatabase().getValueCount());
for (DHTValueEntity dhtValue : nearest.getDatabase().values()) {
assertEquals(valueId, dhtValue.getPrimaryKey());
assertEquals(value, dhtValue.getValue());
assertEquals(creator.getLocalNodeID(), dhtValue.getSecondaryKey());
// The closest Node send us the value!
assertEquals(closest.getLocalNodeID(), dhtValue.getSender().getNodeID());
}
// Pick a Node from the middle of the k-closest Nodes,
// clear its Database and do the same test as with the
// nearest Node above
Context middle = null;
int index = 0;
for (Contact node : evt.getLocations()) {
if (index == k/2) {
middle = (Context)dhts.get(node.getNodeID());
break;
}
index++;
}
assertNotNull(middle);
middle.getDatabase().clear();
assertEquals(0, middle.getDatabase().getKeyCount());
assertEquals(0, middle.getDatabase().getValueCount());
bootstrap(middle, dhts.values());
Thread.sleep(waitForNodes);
assertEquals(0, middle.getDatabase().getKeyCount());
assertEquals(0, middle.getDatabase().getValueCount());
middle.getLocalNode().nextInstanceID();
bootstrap(middle, dhts.values());
Thread.sleep(waitForNodes);
assertEquals(1, middle.getDatabase().getKeyCount());
assertEquals(1, middle.getDatabase().getValueCount());
for (DHTValueEntity dhtValue : middle.getDatabase().values()) {
assertEquals(valueId, dhtValue.getPrimaryKey());
assertEquals(value, dhtValue.getValue());
assertEquals(creator.getLocalNodeID(), dhtValue.getSecondaryKey());
// The nearest Node send us the value
assertEquals(nearest.getLocalNodeID(), dhtValue.getSender().getNodeID());
}
// Check the final state. k + 1 Nodes should have the value!
int count = 0;
dhts.put(nearest.getLocalNodeID(), nearest);
for (MojitoDHT dht : dhts.values()) {
count += ((Context)dht).getDatabase().values().size();
}
// If the creator is a member of the k-closest Nodes then
// make sure we're counting it as well
for (Contact node : evt.getLocations()) {
if (node.getNodeID().equals(creator.getLocalNodeID())) {
count++;
break;
}
}
assertEquals(k + 1, count);
} finally {
for (MojitoDHT dht : dhts.values()) {
dht.close();
}
}
}
/**
* This test will fail if the deletion step of store-forwarding
* is enabled. Its a reminder to test that functionality if
* we ever decide to enable it.
*/
public void testCacheForwardAndDelete() {
DatabaseSettings.DELETE_VALUE_IF_FURTHEST_NODE.revertToDefault();
assertFalse(DatabaseSettings.DELETE_VALUE_IF_FURTHEST_NODE.getValue());
}
public void testStoreMultipleValues() throws Exception {
MojitoDHT dht1 = null;
MojitoDHT dht2 = null;
try {
dht1 = MojitoFactory.createDHT("DHT1");
dht1.bind(2000);
dht1.start();
dht2 = MojitoFactory.createDHT("DHT2");
dht2.bind(2001);
dht2.start();
dht1.bootstrap(new InetSocketAddress("localhost", 2001)).get();
dht2.bootstrap(new InetSocketAddress("localhost", 2000)).get();
Context context1 = (Context)dht1;
KUID primaryKey1 = KUID.createRandomID();
KUID primaryKey2 = KUID.createRandomID();
DHTValue value1 = new DHTValueImpl(DHTValueType.TEST, Version.ZERO, StringUtils.toUTF8Bytes("Hello World"));
DHTValue value2 = new DHTValueImpl(DHTValueType.TEST, Version.ZERO, StringUtils.toUTF8Bytes("Foo Bar"));
DHTValueEntity entity1 = DHTValueEntity.createFromValue(context1, primaryKey1, value1);
DHTValueEntity entity2 = DHTValueEntity.createFromValue(context1, primaryKey2, value2);
Collection<DHTValueEntity> entities = Arrays.asList(entity1, entity2);
DHTFuture<StoreResult> future = context1.store(
dht2.getLocalNode(), null, entities);
/* StoreResult result = */ future.get(); // TODO: should result be tested?
assertEquals(2, dht2.getDatabase().getKeyCount());
assertEquals(2, dht2.getDatabase().getValueCount());
} finally {
if (dht1 != null) { dht1.close(); }
if (dht2 != null) { dht2.close(); }
}
}
/**
* Bootstraps the given Node from one of the other Nodes and makes sure a
* Node isn't trying to bootstrap from itself which would fail.
*/
private static void bootstrap(MojitoDHT dht, Collection<MojitoDHT> dhts)
throws ExecutionException, InterruptedException {
for (MojitoDHT other : dhts) {
if (!dht.getLocalNodeID().equals(other.getLocalNodeID())) {
InetSocketAddress addr = (InetSocketAddress)other.getContactAddress();
dht.bootstrap(new InetSocketAddress("localhost", addr.getPort())).get();
return;
}
}
throw new IllegalStateException("Could not bootstrap: " + dht);
}
}