/*
* 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.manager;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.OnewayExchanger;
import org.limewire.concurrent.SyncWrapper;
import org.limewire.mojito.Context;
import org.limewire.mojito.KUID;
import org.limewire.mojito.concurrent.DHTTask;
import org.limewire.mojito.exceptions.DHTTimeoutException;
import org.limewire.mojito.handler.response.FindNodeResponseHandler;
import org.limewire.mojito.handler.response.PingResponseHandler;
import org.limewire.mojito.handler.response.PingResponseHandler.PingIterator;
import org.limewire.mojito.result.BootstrapResult;
import org.limewire.mojito.result.FindNodeResult;
import org.limewire.mojito.result.PingResult;
import org.limewire.mojito.result.BootstrapResult.ResultType;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.RouteTable;
import org.limewire.mojito.routing.RouteTable.PurgeMode;
import org.limewire.mojito.settings.BootstrapSettings;
import org.limewire.mojito.util.CollectionUtils;
import org.limewire.mojito.util.ContactUtils;
import org.limewire.mojito.util.RouteTableUtils;
import org.limewire.mojito.util.TimeAwareIterable;
/**
* The BootstrapProcess controls the whole process of bootstrapping.
* The sequence looks like this:
* <pre>
* 0) Find a Node that's connected to the DHT
* +--->
* | 1) Lookup own Node ID
* |---2) If there are any Node ID collisions then check 'em,
* | change or Node ID is necessary and start over
* | 3) Refresh all Buckets with prefixed random IDs
* +---4) Prune RouteTable and restart if too many errors in #3
* 5) Done
* </pre>
*/
/* TODO: Step 3 can be done in parallel! It would speed up bootstrapping
* a lot!
*/
class BootstrapProcess implements DHTTask<BootstrapResult> {
private static final Log LOG = LogFactory.getLog(BootstrapProcess.class);
private enum Status { BOOTSTRAPPING, RETRYING_BOOTSTRAP, FINISHED};
private OnewayExchanger<BootstrapResult, ExecutionException> exchanger;
private final Context context;
private final BootstrapManager manager;
/** Serial tasks such as sending collision ping and finding nearest node */
private final List<DHTTask<?>> tasks = new ArrayList<DHTTask<?>>();
/** List of parallel workers executing paralellizable tasks */
private final List<BootstrapWorker> workers = new ArrayList<BootstrapWorker>();
private final SyncWrapper<Status> status = new SyncWrapper<Status>(null);
private volatile boolean foundNewContacts = false;
private int routeTableFailureCount;
private boolean cancelled = false;
private Iterator<KUID> bucketsToRefresh;
private Contact node;
private Set<? extends SocketAddress> dst;
private long startTime = -1L;
private final long waitOnLock;
public BootstrapProcess(Context context, BootstrapManager manager, Contact node) {
this.context = context;
this.manager = manager;
this.node = node;
waitOnLock = BootstrapSettings.getWaitOnLock(true);
}
public BootstrapProcess(Context context, BootstrapManager manager,
Set<? extends SocketAddress> dst) {
this.context = context;
this.manager = manager;
this.dst = dst;
waitOnLock = BootstrapSettings.getWaitOnLock(false);
}
public long getWaitOnLockTimeout() {
return waitOnLock;
}
public void start(OnewayExchanger<BootstrapResult,
ExecutionException> exchanger) {
synchronized(status.getLock()) {
if (status.get() != null)
return;
status.set(Status.BOOTSTRAPPING);
}
if (LOG.isDebugEnabled())
LOG.debug("starting bootstrap "+getPercentage(context.getRouteTable())+"% alive");
if (exchanger == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Starting ResponseHandler without an OnewayExchanger");
}
exchanger = new OnewayExchanger<BootstrapResult, ExecutionException>(true);
}
this.exchanger = exchanger;
startTime = System.currentTimeMillis();
if (node == null) {
findInitialContact();
} else {
findNearestNodes();
}
}
private void findInitialContact() {
OnewayExchanger<PingResult, ExecutionException> c
= new OnewayExchanger<PingResult, ExecutionException>(true) {
@Override
public synchronized void setValue(PingResult value) {
if (LOG.isTraceEnabled()) {
LOG.trace("Found initial bootstrap Node: " + value);
}
super.setValue(value);
handlePong(value);
}
@Override
public synchronized void setException(ExecutionException exception) {
LOG.info("ExecutionException", exception);
super.setException(exception);
exchanger.setException(exception);
}
};
PingResponseHandler handler = new PingResponseHandler(context,
new PingIteratorFactory.SocketAddressPinger(dst));
handler.setMaxErrors(0);
start(handler, c);
}
private void handlePong(PingResult result) {
this.node = result.getContact();
findNearestNodes();
}
private void findNearestNodes() {
OnewayExchanger<FindNodeResult, ExecutionException> c
= new OnewayExchanger<FindNodeResult, ExecutionException>(true) {
@Override
public synchronized void setValue(FindNodeResult value) {
if (LOG.isTraceEnabled()) {
LOG.trace("Found nearest Nodes: " + value);
}
super.setValue(value);
handleNearestNodes(value);
}
@Override
public synchronized void setException(ExecutionException exception) {
super.setException(exception);
handleExecutionException(exception);
}
};
FindNodeResponseHandler handler = new FindNodeResponseHandler(
context, node, context.getLocalNodeID());
start(handler, c);
}
void handleExecutionException(ExecutionException ee) {
LOG.info("ExecutionException", ee);
exchanger.setException(ee);
}
private void handleNearestNodes(FindNodeResult result) {
Collection<? extends Contact> collisions = result.getCollisions();
if (!collisions.isEmpty()) {
checkCollisions(collisions);
} else {
Collection<? extends Contact> path = result.getPath();
// Make sure we found some Nodes
if (path == null || path.isEmpty()) {
bootstrapped(false);
// But other than our local Node
} else if (path.size() == 1
&& path.contains(context.getLocalNode())) {
bootstrapped(false);
// Great! Everything is fine and continue with
// refreshing/filling up the RouteTable by doing
// lookups for random IDs
} else {
refreshAllBuckets();
}
}
}
private void checkCollisions(Collection<? extends Contact> collisions) {
OnewayExchanger<PingResult, ExecutionException> c
= new OnewayExchanger<PingResult, ExecutionException>(true) {
@Override
public synchronized void setValue(PingResult value) {
if (LOG.isErrorEnabled()) {
LOG.error(context.getLocalNode() + " collides with " + value.getContact());
}
super.setValue(value);
handleCollision(value);
}
@Override
public synchronized void setException(ExecutionException exception) {
LOG.info("ExecutionException", exception);
super.setException(exception);
Throwable cause = exception.getCause();
if (cause instanceof DHTTimeoutException) {
// Ignore, everything is fine! Nobody did respond
// and we can keep our Node ID which is good!
// Continue with finding random Node IDs
refreshAllBuckets();
} else {
exchanger.setException(exception);
}
}
};
Contact sender = ContactUtils.createCollisionPingSender(context.getLocalNode());
PingIterator pinger = new PingIteratorFactory.CollisionPinger(
context, sender, org.limewire.collection.CollectionUtils.toSet(collisions));
PingResponseHandler handler
= new PingResponseHandler(context, sender, pinger);
start(handler, c);
}
private void handleCollision(PingResult result) {
// Change our Node ID
context.changeNodeID();
// Start over!
findNearestNodes();
}
/**
* Refresh all Buckets (Phase two)
*
* When we detect that the routing table is stale, we purge it
* and start the bootstrap all over again.
* A stale routing table can be detected by a high number of failures
* during the lookup (alive hosts to expected result set size ratio).
* Note: this only applies to routing tables with more than 1 buckets,
* i.e. routing tables that have more than k nodes.
*/
private void refreshAllBuckets() {
routeTableFailureCount = 0;
foundNewContacts = false;
Collection<KUID> bucketIds = getBucketsToRefresh();
if (LOG.isTraceEnabled()) {
LOG.trace("Buckets to refresh: " + CollectionUtils.toString(bucketIds));
}
bucketsToRefresh = new TimeAwareIterable<KUID>(
BootstrapSettings.BOOTSTRAP_TIMEOUT.getValue(),
bucketIds).iterator();
for (int i = 0; i < BootstrapSettings.BOOTSTRAP_WORKERS.getValue(); i++) {
BootstrapWorker worker = new BootstrapWorker(context, this);
synchronized(this) {
workers.add(worker);
}
context.getDHTExecutorService().execute(worker);
}
}
private Collection<KUID> getBucketsToRefresh() {
List<KUID> bucketIds = org.limewire.collection.CollectionUtils.toList(
context.getRouteTable().getRefreshIDs(true));
Collections.reverse(bucketIds);
return bucketIds;
}
KUID getNextBucket() {
synchronized(this) {
if (cancelled)
return null;
synchronized(bucketsToRefresh) {
if (bucketsToRefresh.hasNext())
return bucketsToRefresh.next();
}
}
boolean determinate = false;
synchronized(status.getLock()) {
if (status.get() != Status.FINISHED) {
status.set(Status.FINISHED);
determinate = true;
}
}
if (determinate)
determinateIfBootstrapped();
return null;
}
private void handleStaleRouteTable() {
LOG.debug("handling stale route table");
// The RouteTable is stale! Remove all non-alive Contacts,
// rebuild the RouteTable and start over!
context.getRouteTable().purge(
PurgeMode.DROP_CACHE,
PurgeMode.PURGE_CONTACTS,
PurgeMode.MERGE_BUCKETS,
PurgeMode.STATE_TO_UNKNOWN);
// And Start over!
findNearestNodes();
}
/**
* Notification that a refresh operation has completed.
* @param failures how many of the pinged nodes failed to respond.
* @param newContacts true if new contacts were discovered.
*/
void refreshDone(int failures, boolean newContacts) {
foundNewContacts |= newContacts;
boolean retry = false;
boolean terminate = false;
synchronized(status.getLock()) {
boolean highFailures = false;
switch(status.get()) {
case BOOTSTRAPPING :
case RETRYING_BOOTSTRAP :
routeTableFailureCount += failures;
if (routeTableFailureCount >= BootstrapSettings.MAX_BOOTSTRAP_FAILURES.getValue()) {
if (LOG.isDebugEnabled())
LOG.debug("high failures: "+routeTableFailureCount);
highFailures = true;
}
}
/*
* at this point we can either retry bootstrapping or terminate it.
*/
if (highFailures) {
switch(status.get()) {
case BOOTSTRAPPING :
routeTableFailureCount = 0;
status.set(Status.RETRYING_BOOTSTRAP);
retry = true;
break;
case RETRYING_BOOTSTRAP :
terminate = true;
status.set(Status.FINISHED);
}
}
}
if (retry)
handleStaleRouteTable();
if (terminate) {
cancel();
determinateIfBootstrapped();
}
}
/**
* Determines whether or not we're bootstrapped.
*/
private void determinateIfBootstrapped() {
boolean bootstrapped = false;
float alive = purgeAndGetPercenetage();
// Check what percentage of the Contacts are alive
if (alive >= BootstrapSettings.IS_BOOTSTRAPPED_RATIO.getValue()) {
bootstrapped = true;
}
if (LOG.isTraceEnabled()) {
LOG.trace("Bootstrapped: " + alive + " >= "
+ BootstrapSettings.IS_BOOTSTRAPPED_RATIO.getValue()
+ " -> " + bootstrapped);
}
bootstrapped(bootstrapped);
}
private float purgeAndGetPercenetage() {
RouteTable routeTable = context.getRouteTable();
synchronized (routeTable) {
routeTable.purge(PurgeMode.DROP_CACHE,
PurgeMode.PURGE_CONTACTS,
PurgeMode.MERGE_BUCKETS);
return getPercentage(routeTable);
}
}
private float getPercentage(RouteTable table) {
return RouteTableUtils.getPercentageOfAliveContacts(table);
}
private void bootstrapped(boolean bootstrapped) {
if (LOG.isTraceEnabled()) {
LOG.trace("Finishing bootstrapping: " + bootstrapped);
}
ResultType type = ResultType.BOOTSTRAP_FAILED;
if (bootstrapped) {
manager.setBootstrapped(true);
type = ResultType.BOOTSTRAP_SUCCEEDED;
}
long time = System.currentTimeMillis() - startTime;
exchanger.setValue(new BootstrapResult(node, time, type));
}
private <T> void start(DHTTask<T> task, OnewayExchanger<T, ExecutionException> c) {
boolean doStart = false;
synchronized (this) {
if (!cancelled) {
tasks.add(task);
doStart = true;
}
}
if (doStart) {
task.start(c);
}
}
public void cancel() {
status.set(Status.FINISHED);
if (LOG.isTraceEnabled()) {
LOG.trace("Canceling BootstrapProcess");
}
List<DHTTask<?>> copy = null;
List<BootstrapWorker> workerCopy = null;
synchronized (this) {
if (!cancelled) {
copy = new ArrayList<DHTTask<?>>(tasks);
tasks.clear();
workerCopy = new ArrayList<BootstrapWorker>(workers);
workers.clear();
cancelled = true;
}
}
if (copy != null) {
for (DHTTask<?> task : copy)
task.cancel();
}
if (workerCopy != null) {
for (BootstrapWorker worker : workerCopy)
worker.shutdown();
}
}
}