/*
* 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.routing.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.PatriciaTrie;
import org.limewire.collection.Trie.Cursor;
import org.limewire.mojito.KUID;
import org.limewire.mojito.concurrent.DHTExecutorService;
import org.limewire.mojito.concurrent.DHTFutureAdapter;
import org.limewire.mojito.concurrent.DHTFutureListener;
import org.limewire.mojito.exceptions.DHTTimeoutException;
import org.limewire.mojito.result.PingResult;
import org.limewire.mojito.routing.Bucket;
import org.limewire.mojito.routing.ClassfulNetworkCounter;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.ContactFactory;
import org.limewire.mojito.routing.RouteTable;
import org.limewire.mojito.routing.Vendor;
import org.limewire.mojito.routing.Version;
import org.limewire.mojito.routing.RouteTable.RouteTableEvent.EventType;
import org.limewire.mojito.settings.RouteTableSettings;
import org.limewire.mojito.util.ContactUtils;
import org.limewire.mojito.util.ExceptionUtils;
import org.limewire.service.ErrorService;
/**
* A PatriciaTrie based RouteTable implementation for the Mojito DHT.
* This is the reference implementation.
*/
public class RouteTableImpl implements RouteTable {
private static final long serialVersionUID = -7351267868357880369L;
private static final Log LOG = LogFactory.getLog(RouteTableImpl.class);
/**
* Trie of Buckets and the Buckets are a Trie of Contacts.
*/
private final PatriciaTrie<KUID, Bucket> bucketTrie;
/**
* A counter for consecutive failures.
*/
private int consecutiveFailures = 0;
/**
* A reference to the ContactPinger.
*/
private transient ContactPinger pinger;
/**
* The local Node.
*/
private Contact localNode;
/**
* A list of RouteTableListeners.
*/
private transient volatile List<RouteTableListener> listeners
= new CopyOnWriteArrayList<RouteTableListener>();
/**
* Executor where to offload notifications to RouteTableListeners.
*/
private transient volatile DHTExecutorService notifier;
/**
* Create a new RouteTable and generates a new random Node ID
* for the local Node.
*/
public RouteTableImpl() {
this(KUID.createRandomID());
}
/**
* Create a new RouteTable and uses the given Node ID
* for the local Node.
*/
public RouteTableImpl(byte[] nodeId) {
this(KUID.createWithBytes(nodeId));
}
/**
* Create a new RouteTable and uses the given Node ID
* for the local Node.
*/
public RouteTableImpl(String nodeId) {
this(KUID.createWithHexString(nodeId));
}
/**
* Create a new RouteTable and uses the given Node ID
* for the local Node.
*/
public RouteTableImpl(KUID nodeId) {
localNode = ContactFactory.createLocalContact(Vendor.UNKNOWN, Version.ZERO, nodeId, 0, false);
bucketTrie = new PatriciaTrie<KUID, Bucket>(KUID.KEY_ANALYZER);
init();
}
/**
* Initializes the RouteTable.
*/
private void init() {
KUID bucketId = KUID.MINIMUM;
Bucket bucket = new BucketNode(this, bucketId, 0);
bucketTrie.put(bucketId, bucket);
addContactToBucket(bucket, localNode);
consecutiveFailures = 0;
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
listeners = new CopyOnWriteArrayList<RouteTableListener>();
// Post-Init the Buckets
for (Bucket bucket : bucketTrie.values()) {
((BucketNode)bucket).postInit();
}
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#setContactPinger(com.limegroup.mojito.routing.RouteTable.ContactPinger)
*/
public void setContactPinger(ContactPinger pinger) {
this.pinger = pinger;
}
public void setNotifier(DHTExecutorService executor) {
this.notifier = executor;
}
/**
* Adds a RouteTableListener.
* <p>
* Implementation Note: The listener(s) is not called from a
* separate event Thread! That means processor intensive tasks
* that are performed straight in the listener(s) can slowdown
* the processing throughput significantly. Offload intensive
* tasks to separate Threads in necessary!
*
* @param l the RouteTableListener instance to add
*/
public void addRouteTableListener(RouteTableListener l) {
if (l == null) {
throw new NullPointerException("RouteTableListener is null");
}
listeners.add(l);
}
/**
* Removes a RouteTableListener.
*
* @param l the RouteTableListener instance to remove
*/
public void removeRouteTableListener(RouteTableListener l) {
if (l == null) {
throw new NullPointerException("RouteTableListener is null");
}
listeners.remove(l);
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#add(com.limegroup.mojito.Contact)
*/
public synchronized void add(Contact node) {
if (localNode.equals(node)) {
String msg = "Cannot add the local Node: " + node;
if (LOG.isErrorEnabled()) {
LOG.error(msg);
}
ErrorService.error(new IllegalArgumentException(msg));
return;
}
// Don't add firewalled Nodes
if (node.isFirewalled()) {
if (LOG.isTraceEnabled()) {
LOG.trace(node + " is firewalled");
}
return;
}
// Make sure we're not mixing IPv4 and IPv6 addresses in the
// RouteTable! IPv6 to IPv4 might work if there's a 6to4 gateway
// or whatsoever but the opposite direction doesn't. An immediate
// idea is to mark IPv6 Nodes as firewalled if they're contacting
// IPv4 Nodes but this would lead to problems in the IPv6 network
// if some IPv6 Nodes don't have access to a 6to4 gateway...
if (!ContactUtils.isSameAddressSpace(localNode, node)) {
// Log as ERROR so that we're not missing this
if (LOG.isErrorEnabled()) {
LOG.error(node + " is from a different IP address space than " + localNode);
}
return;
}
consecutiveFailures = 0;
KUID nodeId = node.getNodeID();
Bucket bucket = bucketTrie.select(nodeId);
Contact existing = bucket.get(nodeId);
if (existing != null) {
updateContactInBucket(bucket, existing, node);
} else if (!bucket.isActiveFull()) {
if (isOkayToAdd(bucket, node)) {
addContactToBucket(bucket, node);
} else {
// only cache node if the bucket can't be split
if (!canSplit(bucket)) {
addContactToBucketCache(bucket, node);
}
}
} else if (split(bucket)) {
add(node); // re-try to add
} else {
replaceContactInBucket(bucket, node);
}
}
/**
* This method updates an existing Contact with data from a new Contact.
* The initial state is that both Contacts have the same Node ID which
* doesn't mean they're really the same Node. In order to figure out
* if they're really equal it's performing some additional checks and
* there are a few side conditions.
*/
protected synchronized void updateContactInBucket(Bucket bucket, Contact existing, Contact node) {
assert (existing.getNodeID().equals(node.getNodeID()));
if (isLocalNode(existing)) {
// The other Node collides with our Node ID! Do nothing,
// the other guy will change its Node ID! If it doesn't
// everybody who has us in their RouteTable will ping us
// to check if we're alive and we're hopefully able to
// respond. Besides that there isn't much we can do. :-/
if (!isLocalNode(node)) {
if (LOG.isDebugEnabled()) {
LOG.debug(node + " collides with " + existing);
}
// Must be instance of LocalContact!
} else if (!(node instanceof LocalContact)) {
String msg = "Attempting to replace the local Node "
+ existing + " with " + node;
if (LOG.isErrorEnabled()) {
LOG.error(msg);
}
throw new IllegalArgumentException(msg);
// Alright, replace the existing Contact with the new
// LocalContact. Log a warning...
} else {
if (LOG.isWarnEnabled()) {
LOG.warn("Updating " + existing + " with " + node);
}
bucket.updateContact(node);
this.localNode = node;
fireContactUpdate(bucket, existing, node);
}
return;
}
/*
* A non-live Contact will never replace a live Contact!
*/
if (existing.isAlive() && !node.isAlive()) {
return;
}
if (!existing.isAlive()
|| isLocalNode(node)
|| existing.equals(node) // <- checks only nodeId + address!
|| ContactUtils.areLocalContacts(existing, node)) {
/*
* See JIRA issue MOJITO-54
*/
node.updateWithExistingContact(existing);
Contact replaced = bucket.updateContact(node);
assert (replaced == existing);
// a good time to ping least recently seen node if we know we
// have a node alive in the replacement cache. Don't do this too often!
long delay = System.currentTimeMillis() - bucket.getTimeStamp();
if (bucket.containsCachedContact(node.getNodeID())
&& (delay > RouteTableSettings.BUCKET_PING_LIMIT.getValue())) {
pingLeastRecentlySeenNode(bucket);
}
touchBucket(bucket);
fireContactUpdate(bucket, existing, node);
} else if (node.isAlive()
&& !existing.hasBeenRecentlyAlive()) {
doSpoofCheck(bucket, existing, node);
}
}
/**
* This method tries to ping the existing Contact and if it doesn't
* respond it will try to replace it with the new Contact. The initial
* state is that both Contacts have the same Node ID.
*/
protected synchronized void doSpoofCheck(Bucket bucket, final Contact existing, final Contact node) {
DHTFutureListener<PingResult> listener = new DHTFutureAdapter<PingResult>() {
@Override
public void handleFutureSuccess(PingResult result) {
if (LOG.isWarnEnabled()) {
LOG.warn(node + " is trying to spoof " + result);
}
// DO NOTHING! The DefaultMessageHandler takes care
// of everything else! DO NOT BAN THE NODE!!!
// Reason: It was maybe just a Node ID collision!
}
@Override
public void handleExecutionException(ExecutionException e) {
DHTTimeoutException timeout = ExceptionUtils.getCause(e, DHTTimeoutException.class);
// We can only make decisions for timeouts!
if (timeout == null) {
return;
}
KUID nodeId = timeout.getNodeID();
SocketAddress address = timeout.getSocketAddress();
if (LOG.isInfoEnabled()) {
LOG.info(ContactUtils.toString(nodeId, address)
+ " did not respond! Replacing it with " + node);
}
synchronized (RouteTableImpl.this) {
Bucket bucket = bucketTrie.select(nodeId);
Contact current = bucket.get(nodeId);
if (current != null && current.equals(existing)) {
/*
* See JIRA issue MOJITO-54
*/
// NOTE: We cannot call updateContactInBucket(...) here
// because it would do the spoof check again.
node.updateWithExistingContact(current);
Contact replaced = bucket.updateContact(node);
assert (replaced == current);
fireContactUpdate(bucket, current, node);
// If the Node is in the Cache then ping the least recently
// seen live Node which might promote the new Node to a
// live Contact!
if (bucket.containsCachedContact(nodeId)) {
pingLeastRecentlySeenNode(bucket);
}
} else {
add(node);
}
}
}
};
fireContactCheck(bucket, existing, node);
ping(existing, listener);
touchBucket(bucket);
}
/**
* This method adds the given Contact to the given Bucket.
*/
protected synchronized void addContactToBucket(Bucket bucket, Contact node) {
bucket.addActiveContact(node);
fireActiveContactAdded(bucket, node);
}
/**
* Adds the given Contact to the Bucket's replacement Cache.
*/
protected synchronized void addContactToBucketCache(Bucket bucket, Contact node) {
if (LOG.isTraceEnabled()) {
LOG.trace("Adding " + node + " to " + bucket + " replacement cache");
}
// If the cache is full the least recently seen
// node will be evicted!
Contact existing = bucket.addCachedContact(node);
fireCachedContactAdded(bucket, existing, node);
}
/**
* Returns true if the bucket can be split.
*/
private boolean canSplit(Bucket bucket) {
// Three conditions for splitting:
// 1. Bucket contains the local Node
// 2. New node part of the smallest subtree to the local node
// 3. current_depth mod symbol_size != 0
boolean containsLocalNode = bucket.contains(getLocalNode().getNodeID());
if (containsLocalNode
|| bucket.isInSmallestSubtree()
|| !bucket.isTooDeep()) {
return true;
}
return false;
}
/**
* This method splits the given Bucket into two new Buckets.
* There are a few conditions in which cases we do split and
* in which cases we don't.
*/
protected synchronized boolean split(Bucket bucket) {
if (canSplit(bucket)) {
if (LOG.isTraceEnabled()) {
LOG.trace("Splitting bucket: " + bucket);
}
List<Bucket> buckets = bucket.split();
assert (buckets.size() == 2);
Bucket left = buckets.get(0);
Bucket right = buckets.get(1);
// The left one replaces the current bucket in the Trie!
Bucket oldLeft = bucketTrie.put(left.getBucketID(), left);
assert (oldLeft == bucket);
// The right one is new in the Trie!
Bucket oldRight = bucketTrie.put(right.getBucketID(), right);
assert (oldRight == null);
fireSplitBucket(bucket, left, right);
// WHOHOOO! WE SPLIT THE BUCKET!!!
return true;
}
return false;
}
/**
* This method tries to replace an existing Contact in the given
* Bucket with the given Contact or tries to add the given Contact
* to the Bucket's replacement Cache. There are certain conditions
* in which cases we replace Contacts and if it's not possible we're
* trying to add the Contact to the replacement cache.
*/
protected synchronized void replaceContactInBucket(Bucket bucket, Contact node) {
if (node.isAlive() && isOkayToAdd(bucket, node)) {
Contact leastRecentlySeen = bucket.getLeastRecentlySeenActiveContact();
// If all Contacts in the given Bucket have the same time
// stamp as the local Node then it's possible that the lrs
// Contact is the local Contact in which case we don't want
// to replace the local Contact with the given Contact
// Is the least recently seen node in UNKNOWN or DEAD state OR is the
// new Node a priority Node AND the lrs Node is NOT the local Node
if (!isLocalNode(leastRecentlySeen)
&& (leastRecentlySeen.isUnknown()
|| leastRecentlySeen.isDead()
|| (node.getTimeStamp() == Contact.PRIORITY_CONTACT))) {
if (LOG.isTraceEnabled()) {
LOG.info("Replacing " + leastRecentlySeen + " with " + node);
}
boolean removed = bucket.removeActiveContact(leastRecentlySeen.getNodeID());
assert (removed == true);
bucket.addActiveContact(node);
touchBucket(bucket);
fireReplaceContact(bucket, leastRecentlySeen, node);
return;
}
}
addContactToBucketCache(bucket, node);
pingLeastRecentlySeenNode(bucket);
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#handleFailure(com.limegroup.mojito.KUID, java.net.SocketAddress)
*/
public synchronized void handleFailure(KUID nodeId, SocketAddress address) {
// NodeID might be null if we sent a ping to
// an unknown Node (i.e. we knew only the
// address) and the ping failed.
if (nodeId == null) {
return;
}
// This should never happen -- who knows?!!
if(nodeId.equals(getLocalNode().getNodeID())) {
if (LOG.isErrorEnabled()) {
LOG.error("Cannot handle local Node's errors: "
+ ContactUtils.toString(nodeId, address));
}
return;
}
Bucket bucket = bucketTrie.select(nodeId);
Contact node = bucket.get(nodeId);
if (node == null) {
// It's neither a live nor a cached Node
// in the bucket!
return;
}
if (!node.getContactAddress().equals(address)) {
if (LOG.isWarnEnabled()) {
LOG.warn(node + " address and " + address + " do not match");
}
return;
}
// Ignore failure if we start getting to many disconnections in a row
if (consecutiveFailures
>= RouteTableSettings.MAX_CONSECUTIVE_FAILURES.getValue()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Ignoring node failure as it appears that we are disconnected");
}
return;
}
consecutiveFailures++;
node.handleFailure();
if (node.isDead()) {
if (bucket.containsActiveContact(nodeId)) {
if (LOG.isTraceEnabled()) {
LOG.trace("Removing " + node + " and replacing it with the MRS Node from Cache");
}
// Remove a live-dead Contact only if there's something
// in the replacement cache or if the Node has too many
// errors
if (bucket.getCacheSize() > 0) {
Contact mrs = null;
while((mrs = bucket.getMostRecentlySeenCachedContact()) != null) {
boolean removed = bucket.removeCachedContact(mrs.getNodeID());
assert (removed == true);
if (isOkayToAdd(bucket, mrs)) {
removed = bucket.removeActiveContact(nodeId);
assert (removed == true);
assert (bucket.isActiveFull() == false);
bucket.addActiveContact(mrs);
fireReplaceContact(bucket, node, mrs);
break;
}
}
} else if (node.getFailures()
>= RouteTableSettings.MAX_ACCEPT_NODE_FAILURES.getValue()){
bucket.removeActiveContact(nodeId);
assert (bucket.isActiveFull() == false);
fireRemoveContact(bucket, node);
}
} else {
// On first glance this might look like as if it is
// not necessary since we're never contacting cached
// Contacts but that's not absolutely true. FIND_NODE
// lookups may return Contacts that are in our cache
// and if they don't respond we want to remove them...
if (LOG.isTraceEnabled()) {
LOG.trace("Removing " + node + " from Cache");
}
boolean removed = bucket.removeCachedContact(nodeId);
assert (removed == true);
}
}
}
/**
* Returns true of it's Okay to add the given Contact to the
* given Bucket as active Contact. See {@link ClassfulNetworkCounter}
* for more information!
*/
protected synchronized boolean isOkayToAdd(Bucket bucket, Contact node) {
ClassfulNetworkCounter counter = bucket.getClassfulNetworkCounter();
boolean okay = (counter == null || counter.isOkayToAdd(node));
if (LOG.isTraceEnabled()) {
if (okay) {
LOG.trace("It's okay to add " + node + " to " + bucket);
} else {
LOG.trace("It's NOT okay to add " + node + " to " + bucket);
}
}
return okay;
}
/**
* Removes the given Contact from the RouteTable.
*/
protected synchronized boolean remove(Contact node) {
return remove(node.getNodeID());
}
/**
* Removes the given KUID (Contact with that KUID)
* from the RouteTable.
*/
protected synchronized boolean remove(KUID nodeId) {
return bucketTrie.select(nodeId).remove(nodeId);
}
/*
* (non-Javadoc)
* @see org.limewire.mojito.routing.RouteTable#getBucket(org.limewire.mojito.KUID)
*/
public synchronized Bucket getBucket(KUID nodeId) {
return bucketTrie.select(nodeId);
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#select(com.limegroup.mojito.KUID)
*/
public synchronized Contact select(final KUID nodeId) {
final Contact[] node = new Contact[] { null };
bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
node[0] = entry.getValue().select(nodeId);
if (node[0] != null) {
return SelectStatus.EXIT;
}
return SelectStatus.CONTINUE;
}
});
return node[0];
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#get(com.limegroup.mojito.KUID)
*/
public synchronized Contact get(KUID nodeId) {
return bucketTrie.select(nodeId).get(nodeId);
}
/**
* Returns 'count' number of Contacts that are nearest (XOR distance)
* to the given KUID.
*/
public synchronized Collection<Contact> select(KUID nodeId, int count) {
return select(nodeId, count, SelectMode.ALL);
}
/*
* (non-Javadoc)
* @see org.limewire.mojito.routing.RouteTable#select(org.limewire.mojito.KUID, int, org.limewire.mojito.routing.RouteTable.SelectMode)
*/
public synchronized Collection<Contact> select(final KUID nodeId, final int count,
final SelectMode mode) {
if (count == 0) {
return Collections.emptyList();
}
final int maxNodeFailures = RouteTableSettings.MAX_ACCEPT_NODE_FAILURES.getValue();
final List<Contact> nodes = new ArrayList<Contact>(count);
bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
Bucket bucket = entry.getValue();
Collection<Contact> list = null;
if (mode == SelectMode.ALIVE
|| mode == SelectMode.ALIVE_WITH_LOCAL) {
// Select all Contacts from the Bucket to compensate
// the fact that not all of them will be alive. We're
// using Bucket.select() instead of Bucket.getActiveContacts()
// to get the Contacts sorted by xor distance!
list = bucket.select(nodeId, bucket.getActiveSize());
} else {
list = bucket.select(nodeId, count);
}
for(Contact node : list) {
// Exit the loop if done
if (nodes.size() >= count) {
return SelectStatus.EXIT;
}
// Ignore all non-alive Contacts if only
// active Contacts are requested.
// We also ignore the local contact here (see LocalContact.isAlive)
// because a node will always have himself in the routing table
if (mode == SelectMode.ALIVE && !node.isAlive()) {
continue;
}
if (mode == SelectMode.ALIVE_WITH_LOCAL
&& !node.isAlive()
&& !isLocalNode(node)) {
continue;
}
// Ignore all Contacts that are down
if (node.isShutdown()) {
continue;
}
if (node.isDead()) {
float fact = (maxNodeFailures - node.getFailures())
/ (float)Math.max(1, maxNodeFailures);
if (Math.random() >= fact) {
continue;
}
}
nodes.add(node);
}
return SelectStatus.CONTINUE;
}
});
assert (nodes.size() <= count) : "Expected " + count + " or less elements but is " + nodes.size();
return nodes;
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getContacts()
*/
public synchronized Collection<Contact> getContacts() {
Collection<Contact> live = getActiveContacts();
Collection<Contact> cached = getCachedContacts();
List<Contact> nodes = new ArrayList<Contact>(live.size() + cached.size());
nodes.addAll(live);
nodes.addAll(cached);
return nodes;
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getActiveContacts()
*/
public synchronized Collection<Contact> getActiveContacts() {
List<Contact> nodes = new ArrayList<Contact>();
for (Bucket bucket : bucketTrie.values()) {
nodes.addAll(bucket.getActiveContacts());
}
return nodes;
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getCachedContacts()
*/
public synchronized Collection<Contact> getCachedContacts() {
List<Contact> nodes = new ArrayList<Contact>();
for (Bucket bucket : bucketTrie.values()) {
nodes.addAll(bucket.getCachedContacts());
}
return nodes;
}
/*
* If we are bootstrapping, we don't want to refresh the bucket
* that contains the local node ID, as phase 1 already takes
* care of this. Additionally, when we bootstrap, we don't
* look at the bucket's timestamp (isRefreshRequired) so
* that we randomly fill up our routing table.
*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getRefreshIDs(boolean)
*/
public synchronized Collection<KUID> getRefreshIDs(final boolean bootstrapping) {
final KUID nodeId = getLocalNode().getNodeID();
final List<KUID> randomIds = new ArrayList<KUID>();
bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
Bucket bucket = entry.getValue();
// Don't refresh the local Bucket if we're bootstrapping
// since phase one takes already care of it.
if (bootstrapping && bucket.contains(getLocalNode().getNodeID())) {
return SelectStatus.CONTINUE;
}
if (bootstrapping || bucket.isRefreshRequired()) {
// Select a random ID with this prefix
KUID randomId = KUID.createPrefxNodeID(
bucket.getBucketID(), bucket.getDepth());
if(LOG.isTraceEnabled()) {
LOG.trace("Refreshing bucket:" + bucket
+ " with random ID: " + randomId);
}
randomIds.add(randomId);
touchBucket(bucket);
}
return SelectStatus.CONTINUE;
}
});
return randomIds;
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getBuckets()
*/
public synchronized Collection<Bucket> getBuckets() {
return Collections.unmodifiableCollection(bucketTrie.values());
}
/**
* Touches the given Bucket (i.e. updates its timeStamp).
*/
private void touchBucket(Bucket bucket) {
if(LOG.isTraceEnabled()) {
LOG.trace("Touching bucket: " + bucket);
}
bucket.touch();
}
/**
* Pings the least recently seen active Contact in the given Bucket.
*/
private void pingLeastRecentlySeenNode(Bucket bucket) {
Contact lrs = bucket.getLeastRecentlySeenActiveContact();
if (!isLocalNode(lrs)) {
ping(lrs, null);
}
}
/**
* Pings the given Contact and adds the given DHTEventListener to
* the DHTFuture if it's not null.
*/
private void ping(Contact node, DHTFutureListener<PingResult> listener) {
ContactPinger pinger = this.pinger;
if (pinger != null) {
pinger.ping(node, listener);
} else {
handleFailure(node.getNodeID(), node.getContactAddress());
if (listener != null) {
ExecutionException exception = new ExecutionException(
new DHTTimeoutException(node.getNodeID(), node.getContactAddress(), null, 0L));
listener.handleExecutionException(exception);
}
}
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#getLocalNode()
*/
public Contact getLocalNode() {
if (localNode == null) {
throw new IllegalStateException("RouteTable is not initialized");
}
return localNode;
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#isLocalNode(com.limegroup.mojito.Contact)
*/
public boolean isLocalNode(Contact node) {
return node.equals(getLocalNode());
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#size()
*/
public synchronized int size() {
return getActiveContacts().size() + getCachedContacts().size();
}
/*
* (non-Javadoc)
* @see com.limegroup.mojito.routing.RouteTable#clear()
*/
public synchronized void clear() {
bucketTrie.clear();
fireClear();
init();
}
/*
* (non-Javadoc)
* @see org.limewire.mojito.routing.RouteTable#purge(long)
*/
public synchronized void purge(long elapsedTimeSinceLastContact) {
if (localNode == null) {
throw new IllegalStateException("RouteTable is not initialized");
}
if (elapsedTimeSinceLastContact == -1L) {
return;
}
long currentTime = System.currentTimeMillis();
for (Contact node : getActiveContacts()) {
if (isLocalNode(node)) {
continue;
}
if ((currentTime - node.getTimeStamp()) < elapsedTimeSinceLastContact) {
continue;
}
remove(node);
}
for (Contact node : getCachedContacts()) {
if ((currentTime - node.getTimeStamp()) < elapsedTimeSinceLastContact) {
continue;
}
remove(node);
}
mergeBuckets();
}
/*
* (non-Javadoc)
* @see org.limewire.mojito.routing.RouteTable#purge(org.limewire.mojito.routing.RouteTable.PurgeMode, org.limewire.mojito.routing.RouteTable.PurgeMode[])
*/
public synchronized void purge(PurgeMode first, PurgeMode... rest) {
if (localNode == null) {
throw new IllegalStateException("RouteTable is not initialized");
}
EnumSet<PurgeMode> modes = EnumSet.of(first, rest);
if (modes.contains(PurgeMode.DROP_CACHE)) {
dropCache();
}
if (modes.contains(PurgeMode.PURGE_CONTACTS)) {
purgeContacts();
}
if (modes.contains(PurgeMode.MERGE_BUCKETS)) {
mergeBuckets();
}
if (modes.contains(PurgeMode.STATE_TO_UNKNOWN)) {
changeStateToUnknown(getActiveContacts());
changeStateToUnknown(getCachedContacts());
}
}
private synchronized void dropCache() {
for (Contact node : getCachedContacts()) {
remove(node);
}
}
private synchronized void purgeContacts() {
bucketTrie.traverse(new Cursor<KUID, Bucket>() {
public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
Bucket bucket = entry.getValue();
bucket.purge();
return SelectStatus.CONTINUE;
}
});
}
private synchronized void mergeBuckets() {
// Get the active Contacts
Collection<Contact> activeNodes = getActiveContacts();
activeNodes = ContactUtils.sortAliveToFailed(activeNodes);
// Get the cached Contacts
Collection<Contact> cachedNodes = getCachedContacts();
cachedNodes = ContactUtils.sort(cachedNodes);
// We count on the fact that getActiveContacts() and
// getCachedContacts() return copies!
clear();
// Remove the local Node from the List. Shouldn't fail as
// activeNodes is a copy!
boolean removed = activeNodes.remove(localNode);
assert (removed);
// Re-add the active Contacts
for (Contact node : activeNodes) {
add(node);
}
// And re-add the cached Contacts
for (Contact node : cachedNodes) {
add(node);
}
}
private synchronized void changeStateToUnknown(Collection<Contact> nodes) {
for (Contact node : nodes) {
node.unknown();
}
}
protected void fireActiveContactAdded(Bucket bucket, Contact node) {
fireRouteTableEvent(bucket, null, null, null, node, EventType.ADD_ACTIVE_CONTACT);
}
protected void fireCachedContactAdded(Bucket bucket, Contact existing, Contact node) {
fireRouteTableEvent(bucket, null, null, existing, node, EventType.ADD_CACHED_CONTACT);
}
protected void fireContactUpdate(Bucket bucket, Contact existing, Contact node) {
fireRouteTableEvent(bucket, null, null, existing, node, EventType.UPDATE_CONTACT);
}
protected void fireReplaceContact(Bucket bucket, Contact existing, Contact node) {
fireRouteTableEvent(bucket, null, null, existing, node, EventType.REPLACE_CONTACT);
}
protected void fireRemoveContact(Bucket bucket, Contact node) {
fireRouteTableEvent(bucket, null, null, null, node, EventType.REMOVE_CONTACT);
}
protected void fireContactCheck(Bucket bucket, Contact existing, Contact node) {
fireRouteTableEvent(bucket, null, null, existing, node, EventType.CONTACT_CHECK);
}
protected void fireSplitBucket(Bucket bucket, Bucket left, Bucket right) {
fireRouteTableEvent(bucket, left, right, null, null, EventType.SPLIT_BUCKET);
}
protected void fireClear() {
fireRouteTableEvent(null, null, null, null, null, EventType.CLEAR);
}
protected void fireRouteTableEvent(Bucket bucket, Bucket left, Bucket right,
Contact existing, Contact node, EventType type) {
if (listeners.isEmpty()) {
return;
}
final RouteTableEvent event = new RouteTableEvent(
this, bucket, left, right, existing, node, type);
Runnable r = new Runnable() {
public void run() {
for (RouteTableListener listener : listeners) {
listener.handleRouteTableEvent(event);
}
}
};
DHTExecutorService e = notifier;
if (e != null)
e.executeSequentially(r);
else
r.run();
}
@Override
public synchronized String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append("Local: ").append(getLocalNode()).append("\n");
int alive = 0;
int dead = 0;
int down = 0;
int unknown = 0;
for(Bucket bucket : getBuckets()) {
buffer.append(bucket).append("\n");
for (Contact node : bucket.getActiveContacts()) {
if (node.isShutdown()) {
down++;
}
if (node.isAlive()) {
alive++;
} else if (node.isDead()) {
dead++;
} else {
unknown++;
}
}
for (Contact node : bucket.getCachedContacts()) {
if (node.isShutdown()) {
down++;
}
if (node.isAlive()) {
alive++;
} else if (node.isDead()) {
dead++;
} else {
unknown++;
}
}
}
buffer.append("Total Buckets: ").append(bucketTrie.size()).append("\n");
buffer.append("Total Active Contacts: ").append(getActiveContacts().size()).append("\n");
buffer.append("Total Cached Contacts: ").append(getCachedContacts().size()).append("\n");
buffer.append("Total Alive Contacts: ").append(alive).append("\n");
buffer.append("Total Dead Contacts: ").append(dead).append("\n");
buffer.append("Total Down Contacts: ").append(down).append("\n");
buffer.append("Total Unknown Contacts: ").append(unknown).append("\n");
return buffer.toString();
}
}