/*
* Copyright 2009-2012 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ardverk.dht.io;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.inject.Provider;
import org.ardverk.concurrent.AsyncFuture;
import org.ardverk.concurrent.ExecutorUtils;
import org.ardverk.concurrent.FutureUtils;
import org.ardverk.dht.KUID;
import org.ardverk.dht.config.NodeConfig;
import org.ardverk.dht.entity.LookupEntity;
import org.ardverk.dht.message.MessageType;
import org.ardverk.dht.message.ResponseMessage;
import org.ardverk.dht.routing.Contact;
import org.ardverk.dht.routing.RouteTable;
import org.ardverk.dht.utils.XorComparator;
import org.ardverk.lang.TimeStamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LookupResponseHandler} implements the base logic for the
* {@link MessageType#FIND_NODE} and {@link MessageType#FIND_VALUE} logic.
*/
abstract class LookupResponseHandler<T extends LookupEntity>
extends AbstractResponseHandler<T> {
private static final Logger LOG
= LoggerFactory.getLogger(LookupResponseHandler.class);
private static final ScheduledThreadPoolExecutor EXECUTOR
= ExecutorUtils.newSingleThreadScheduledExecutor("BoostThread");
private final TimeStamp creationTime = TimeStamp.now();
protected final NodeConfig config;
private final LookupManager lookupManager;
private final ProcessCounter lookupCounter;
private ScheduledFuture<?> boostFuture;
public LookupResponseHandler(Provider<MessageDispatcher> messageDispatcher,
Contact[] contacts, RouteTable routeTable, KUID lookupId,
NodeConfig config) {
super(messageDispatcher);
this.config = config;
lookupManager = new LookupManager(contacts, routeTable, lookupId);
lookupCounter = new ProcessCounter(config.getAlpha());
}
@Override
protected synchronized void go(AsyncFuture<T> future) throws IOException {
// NOTE: We assume we're holding the future lock!
assert (Thread.holdsLock(future));
long boostFrequency = config.getBoostFrequencyInMillis();
if (0L < boostFrequency) {
Runnable task = new Runnable() {
@Override
public void run() {
try {
boost();
} catch (IOException err) {
LOG.error("IOException", err);
}
}
};
boostFuture = EXECUTOR.scheduleWithFixedDelay(
task, boostFrequency, boostFrequency,
TimeUnit.MILLISECONDS);
}
process(0);
}
@Override
protected synchronized void done() {
FutureUtils.cancel(boostFuture, true);
}
/**
* Kicks off an additional lookup if we haven't received any
* responses for a while.
*
* NOTE: This is called from a different {@link Thread}! We must
* therefore pay great attention to the locking order and acquire
* the locks in the same order as the other methods!
*/
private void boost() throws IOException {
synchronized (future) {
if (future.isDone()) {
return;
}
synchronized (this) {
if (lookupManager.hasNext(true)) {
long boostTimeout = config.getBoostTimeoutInMillis();
if (boostTimeout >= 0L && getLastResponseTimeInMillis() >= boostTimeout) {
try {
Contact contact = lookupManager.next();
lookup(contact);
lookupCounter.increment(true);
} finally {
postProcess();
}
}
}
}
}
}
/**
* The {@link #process(int)} method is the heart of the lookup process.
*/
private synchronized void process(int decrement) throws IOException {
try {
preProcess(decrement);
while (lookupCounter.hasNext()) {
if (!lookupManager.hasNext()) {
break;
}
Contact contact = lookupManager.next();
lookup(contact);
lookupCounter.increment();
}
} finally {
postProcess();
}
}
/**
* Sends a lookup request to the given {@link Contact}.
*/
private void lookup(Contact dst) throws IOException {
long defaultTimeout = config.getLookupTimeoutInMillis();
long adaptiveTimeout = config.getAdaptiveTimeout(
dst, defaultTimeout, TimeUnit.MILLISECONDS);
lookup(dst, lookupManager.lookupId, adaptiveTimeout, TimeUnit.MILLISECONDS);
}
/**
* Called by {@link #process(int)} before it is executing its own code.
*/
private synchronized void preProcess(int decrement) {
while (0 < decrement--) {
lookupCounter.decrement();
}
}
/**
* Called by {@link #process(int)} after it has executed its own code.
*/
private synchronized void postProcess() {
int count = lookupCounter.getActive();
if (count == 0) {
complete(createOutcome());
}
}
/**
* Sends a lookup request to the given {@link Contact}.
*/
protected abstract void lookup(Contact dst, KUID lookupId,
long timeout, TimeUnit unit) throws IOException;
/**
* Called upon completion.
*/
protected abstract void complete(Outcome outcome);
@Override
protected final synchronized void processResponse(RequestEntity entity,
ResponseMessage response, long time, TimeUnit unit)
throws IOException {
try {
processResponse0(entity, response, time, unit);
} finally {
process(1);
}
}
/**
* @see #processResponse(RequestEntity, ResponseMessage, long, TimeUnit)
*/
protected abstract void processResponse0(RequestEntity entity,
ResponseMessage response, long time, TimeUnit unit) throws IOException;
/**
* Adds the given {@link Contact}s to the lookup's internal processing
* queue.
*/
protected synchronized void processContacts(Contact src,
Contact[] contacts, long time, TimeUnit unit) throws IOException {
lookupManager.handleResponse(src, contacts, time, unit);
}
@Override
protected final synchronized void processTimeout(RequestEntity entity,
long time, TimeUnit unit) throws IOException {
try {
processTimeout0(entity, time, unit);
} finally {
process(1);
}
}
/**
* @see #processTimeout(RequestEntity, long, TimeUnit)
*/
protected synchronized void processTimeout0(RequestEntity entity,
long time, TimeUnit unit) throws IOException {
lookupManager.handleTimeout(time, unit);
}
@Override
protected final void processIllegalResponse(RequestEntity entity,
ResponseMessage response, long time, TimeUnit unit)
throws IOException {
try {
processIllegalResponse0(entity, response, time, unit);
} finally {
process(1);
}
}
protected synchronized void processIllegalResponse0(RequestEntity entity,
ResponseMessage response, long time, TimeUnit unit)
throws IOException {
// Do nothing!
}
@Override
protected final void processException(RequestEntity entity, Throwable exception) {
try {
processException0(entity, exception);
} finally {
try {
process(1);
} catch (IOException err) {
setException(err);
}
}
}
protected synchronized void processException0(RequestEntity entity, Throwable exception) {
// Do nothing!
}
/**
* Creates and returns the current lookup {@link Outcome}.
*/
protected synchronized Outcome createOutcome() {
return new Outcome() {
private final long time = creationTime.getAgeInMillis();
private final Contact[] closest = lookupManager.getClosest();
private final Contact[] contacts = lookupManager.getContacts();
private final int hop = lookupManager.getHop();
private final int timeouts = lookupManager.getErrorCount();
@Override
public KUID getId() {
return lookupManager.lookupId;
}
@Override
public Contact[] getClosest() {
return closest;
}
@Override
public Contact[] getContacts() {
return contacts;
}
@Override
public int getHop() {
return hop;
}
@Override
public int getErrorCount() {
return timeouts;
}
@Override
public long getTime(TimeUnit unit) {
return unit.convert(time, TimeUnit.MILLISECONDS);
}
};
}
/**
* The {@link LookupManager} controls the lookup process.
*/
private class LookupManager {
private final boolean exhaustive = config.isExhaustive();
private final boolean randomize = config.isRandomize();
private final RouteTable routeTable;
private final KUID lookupId;
/**
* A {@link Set} of all responses
*/
private final NavigableSet<Contact> responses;
/**
* A {@link Set} of the k-closest responses
*/
private final NavigableSet<Contact> closest;
/**
* A {@link Set} of {@link Contact}s to query
*/
private final NavigableSet<Contact> query;
/**
* A history of all {@link KUID}s that were added to the
* {@link #query} {@link NavigableSet}.
*/
private final Map<KUID, Integer> history = new HashMap<>();
private int currentHop = 0;
private int timeouts = 0;
public LookupManager(Contact[] contacts, RouteTable routeTable, KUID lookupId) {
this.routeTable = routeTable;
this.lookupId = lookupId;
Contact localhost = routeTable.getIdentity();
KUID contactId = localhost.getId();
XorComparator comparator = new XorComparator(lookupId);
this.responses = new TreeSet<Contact>(comparator);
this.closest = new TreeSet<Contact>(comparator);
this.query = new TreeSet<Contact>(comparator);
history.put(contactId, 0);
addToResponses(localhost);
for (Contact contact : contacts) {
addToQuery(contact, 1);
}
}
public void handleResponse(Contact src, Contact[] contacts,
long time, TimeUnit unit) {
boolean success = addToResponses(src);
if (!success) {
return;
}
for (Contact contact : contacts) {
if (addToQuery(contact, currentHop+1)) {
routeTable.add(contact);
}
}
}
public void handleTimeout(long time, TimeUnit unit) {
timeouts++;
}
public Contact[] getClosest() {
int length = Math.min(routeTable.getK(), responses.size());
Contact[] closest = new Contact[length];
Iterator<Contact> it = responses.iterator();
for (int i = 0; i < closest.length; i++) {
closest[i] = it.next();
}
return closest;
}
public Contact[] getContacts() {
return responses.toArray(new Contact[0]);
}
public int getHop() {
return currentHop;
}
public int getErrorCount() {
return timeouts;
}
private boolean addToResponses(Contact contact) {
if (responses.add(contact)) {
closest.add(contact);
if (closest.size() > routeTable.getK()) {
closest.pollLast();
}
KUID contactId = contact.getId();
currentHop = history.get(contactId);
return true;
}
return false;
}
private boolean addToQuery(Contact contact, int hop) {
KUID contactId = contact.getId();
if (!history.containsKey(contactId)) {
history.put(contactId, hop);
query.add(contact);
return true;
}
return false;
}
private boolean isCloserThanClosest(Contact other) {
if (!closest.isEmpty()) {
Contact contact = closest.last();
KUID contactId = contact.getId();
KUID otherId = other.getId();
return otherId.isCloserTo(lookupId, contactId);
}
return true;
}
public boolean hasNext() {
return hasNext(false);
}
public boolean hasNext(boolean force) {
if (!query.isEmpty()) {
Contact contact = query.first();
if (force || exhaustive
|| closest.size() < routeTable.getK()
|| isCloserThanClosest(contact)) {
return true;
}
}
return false;
}
public Contact next() {
Contact contact = null;
if (randomize && !query.isEmpty()) {
// Knuth: Can we pick a random element from a set of
// items whose cardinality we do not know?
//
// Pick an item and store it. Pick the next one, and replace
// the first one with it with probability 1/2. Pick the third
// one, and do a replace with probability 1/3, and so on. At
// the end, the item you've stored has a probability of 1/n
// of being any particular element.
//
// NOTE: We do know the cardinality but we don't have the
// required methods to retrieve elements from the Set and
// are forced to use the Iterator (streaming) as described
// above.
int index = 0;
for (Contact c : query) {
if (index >= routeTable.getK()) {
break;
}
// First element is always true because 1/1 >= random[0..1]!
if (1d/++index >= Math.random()) {
contact = c;
}
}
query.remove(contact);
} else {
contact = query.pollFirst();
}
if (contact == null) {
throw new NoSuchElementException();
}
return contact;
}
}
}