/*
* 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.handler;
import java.net.SocketAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.mojito.Context;
import org.limewire.mojito.KUID;
import org.limewire.mojito.db.DHTValueEntity;
import org.limewire.mojito.db.Database;
import org.limewire.mojito.messages.DHTMessage;
import org.limewire.mojito.messages.PingResponse;
import org.limewire.mojito.messages.RequestMessage;
import org.limewire.mojito.messages.ResponseMessage;
import org.limewire.mojito.messages.SecurityTokenProvider;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.RouteTable;
import org.limewire.mojito.routing.RouteTable.SelectMode;
import org.limewire.mojito.settings.DatabaseSettings;
import org.limewire.mojito.settings.KademliaSettings;
import org.limewire.mojito.settings.StoreSettings;
import org.limewire.mojito.statistics.DatabaseStatisticContainer;
import org.limewire.mojito.util.ContactUtils;
import org.limewire.security.SecurityToken;
/**
* Performs basic Kademlia {@link RouteTable}
* update operations. That means adding new Nodes if the <code>RouteTable</code>
* is not full, updating the last seen time stamp of Nodes and so forth.
*/
public class DefaultMessageHandler {
private static final Log LOG = LogFactory.getLog(DefaultMessageHandler.class);
private static enum Operation {
// Do nothing
NOTHING,
// Forward value
FORWARD,
// Delete value
DELETE;
}
private DatabaseStatisticContainer databaseStats;
protected final Context context;
public DefaultMessageHandler(Context context) {
this.context = context;
databaseStats = context.getDatabaseStats();
}
public void handleResponse(ResponseMessage message, long time) {
addLiveContactInfo(message.getContact(), message);
}
public void handleLateResponse(ResponseMessage message) {
Contact node = message.getContact();
if (!node.isFirewalled()) {
context.getRouteTable().add(node); // update
}
}
public void handleTimeout(KUID nodeId, SocketAddress dst,
RequestMessage message, long time) {
context.getRouteTable().handleFailure(nodeId, dst);
}
public void handleRequest(RequestMessage message) {
addLiveContactInfo(message.getContact(), message);
}
/**
* Adds the given <code>Contact</code> or updates it if it's already in our
* <code>RouteTable</code>
*/
private synchronized void addLiveContactInfo(Contact node, DHTMessage message) {
RouteTable routeTable = context.getRouteTable();
// If the Node is going to shutdown then don't bother
// further than this.
if (node.isShutdown()) {
if (LOG.isInfoEnabled()) {
LOG.info(node + " is going to shut down");
}
synchronized (routeTable) {
// Make sure there's an existing Contact in the RouteTable.
// Otherwise don't bother!
Contact existing = routeTable.get(node.getNodeID());
if (node.equals(existing)) {
// Update the new Contact in the RouteTable and
// mark it as shutdown
// mark the existing contact as shutdown if its alive or
// it will not be removed.
if (existing.isAlive())
existing.shutdown(true);
routeTable.add(node);
node.shutdown(true);
}
}
return;
}
// Ignore firewalled Nodes
if (node.isFirewalled()) {
if (LOG.isInfoEnabled()) {
LOG.info(node + " is firewalled");
}
return;
}
if (ContactUtils.isPrivateAddress(node)) {
if (LOG.isInfoEnabled()) {
LOG.info(node + " has a private address");
}
return;
}
KUID nodeId = node.getNodeID();
if (context.isLocalNodeID(nodeId)) {
// This is expected if there's a Node ID collision
assert (message instanceof PingResponse)
: "Expected a PingResponse but got a " + message.getClass()
+ " from " + message.getContact();
if (LOG.isInfoEnabled()) {
LOG.info("Looks like our NodeID collides with " + node);
}
return;
}
if (StoreSettings.STORE_FORWARD_ENABLED.getValue()) {
// Only do store forward if it is a new node in our routing table
// (we are (re)connecting to the network) or a node that is reconnecting
Contact existing = routeTable.get(nodeId);
if (existing == null
|| existing.isDead()
|| existing.getInstanceID() != node.getInstanceID()) {
// Store forward only if we're bootstrapped
if (context.isBootstrapped()) {
int k = KademliaSettings.REPLICATION_PARAMETER.getValue();
//we select the 2*k closest nodes in order to also check those values
//where the local node is part of the k closest to the value but not part
//of the k closest to the new joining node.
Collection<Contact> nodes = routeTable.select(nodeId, 2*k, SelectMode.ALL);
// Are we one of the K nearest Nodes to the contact?
if (containsNodeID(nodes, context.getLocalNodeID())) {
if (LOG.isTraceEnabled()) {
LOG.trace("Node " + node + " is new or has changed his instanceID, will check for store forward!");
}
forwardOrRemoveValues(node, existing, message);
}
}
}
}
// Add the Node to our RouteTable or if it's
// already there update its timeStamp and whatsoever
routeTable.add(node);
}
/**
* This method depends on addLiveContactInfo(...) and does two things.
* It either forwards or removes a DHTValue it from the local Database.
* For details see Kademlia spec!
*/
private void forwardOrRemoveValues(Contact node, Contact existing, DHTMessage message) {
List<DHTValueEntity> valuesToForward = new ArrayList<DHTValueEntity>();
Database database = context.getDatabase();
synchronized(database) {
for(KUID primaryKey : database.keySet()) {
Operation op = getOperation(node, existing, primaryKey);
if (LOG.isDebugEnabled())
LOG.debug("node: " + node + "existing: " + existing + "operation: " + op);
if (op.equals(Operation.FORWARD)) {
Map<KUID, DHTValueEntity> bag = database.get(primaryKey);
valuesToForward.addAll(bag.values());
databaseStats.STORE_FORWARD_COUNT.incrementStat();
} else if (op.equals(Operation.DELETE)
&& DatabaseSettings.DELETE_VALUE_IF_FURTHEST_NODE.getValue()) {
Map<KUID, DHTValueEntity> bag = database.get(primaryKey);
for (DHTValueEntity entity : bag.values()) {
//System.out.println("REMOVING: " + entity + "\n");
database.remove(entity.getPrimaryKey(), entity.getSecondaryKey());
}
databaseStats.STORE_FORWARD_REMOVALS.incrementStat();
}
}
}
if (!valuesToForward.isEmpty()) {
SecurityToken securityToken = null;
if (message instanceof SecurityTokenProvider) {
securityToken = ((SecurityTokenProvider)message).getSecurityToken();
if (securityToken == null
&& StoreSettings.STORE_REQUIRES_SECURITY_TOKEN.getValue()) {
if (LOG.isInfoEnabled()) {
LOG.info(node + " sent us a null SecurityToken");
}
return;
}
}
context.store(node, securityToken, valuesToForward);
}
}
/**
* Returns whether or not the local Node is in the given List
*/
private boolean containsNodeID(Collection<Contact> nodes, KUID id) {
for (Contact node : nodes) {
if (id.equals(node.getNodeID())) {
return true;
}
}
return false;
}
/**
* Determines whether to remove, forward or to do nothing with the
* value that is associated with the given valueId.
*/
private Operation getOperation(Contact node, Contact existing, KUID valueId) {
// To avoid redundant STORE forward, a node only transfers a value
// if it is the closest to the key or if its ID is closer than any
// other ID (except the new closest one of course)
// TODO: maybe relax this a little bit: what if we're not the closest
// and the closest is stale?
int k = KademliaSettings.REPLICATION_PARAMETER.getValue();
RouteTable routeTable = context.getRouteTable();
List<Contact> nodes = org.limewire.collection.CollectionUtils.toList(
routeTable.select(valueId, k, SelectMode.ALL));
Contact closest = nodes.get(0);
Contact furthest = nodes.get(nodes.size()-1);
if (LOG.isDebugEnabled()) {
LOG.debug(MessageFormat.format("node: {0}, existing: {1}, close nodes: {2}", node, existing, nodes));
}
// StringBuilder sb = new StringBuilder();
// sb.append("ME: "+context.getLocalNode()+"\n");
// sb.append("Them: "+node).append("\n");
// sb.append("RT nearest: " + closest).append("\n");
// sb.append("RT furthest: " + furthest).append("\n");
// sb.append(CollectionUtils.toString(nodes)).append("\n");
// We store forward if:
// #1 We're the nearest Node of the k-closest Nodes to
// the given valueId
//
// #2 We're the second nearest of the k-closest Nodes to
// the given valueId AND the other Node is the nearest.
// In other words it changed its instance ID 'cause it
// was offline for a short period of time or whatsoever.
// (see also pre-condition(s) from where we're calling
// this method)
//
// The first condition applies if the Node is new
// and we're the closest Node. The second condition
// applies if the Node has changed it's instanceId.
// That means we're the second closest and since
// the other Node has changed its instanceId we must
// re-send the values
if (context.isLocalNode(closest)
|| (node.equals(closest)
&& nodes.size() > 1
&& context.isLocalNode(nodes.get(1)))) {
KUID nodeId = node.getNodeID();
KUID furthestId = furthest.getNodeID();
// #3 The other Node must be equal to the furthest Node
// or better
if (nodeId.equals(furthestId)
|| nodeId.isNearerTo(valueId, furthestId)) {
// sb.append("CONDITION B (FORWARD)").append("\n");
// sb.append("Local (from): " + context.getLocalNode()).append("\n");
// sb.append("Remote (to): " + node).append("\n");
// sb.append(CollectionUtils.toString(nodes)).append("\n");
// System.out.println(sb.toString());
if (LOG.isTraceEnabled()) {
LOG.trace("Node " + node + " is now close enough to a value and we are responsible for xfer");
}
return Operation.FORWARD;
}
// We remove a value if:
// #1 The value is stored at k Nodes
// (i.e. the total number of Nodes in the DHT
// is equal or greater than k. If the DHT has
// less than k Nodes then there's no reason to
// remove a value)
//
// #2 This Node is the furthest of the k-closest Nodes
//
// #3 The new Node isn't in our RouteTable yet. That means
// adding it will push this Node out of the club of the
// k-closest Nodes and makes it the (k+1)-closest Node.
//
// #4 The new Node is nearer to the given valueId then
// the furthest away Node (we).
} else if (nodes.size() >= k
&& context.isLocalNode(furthest)
&& (existing == null || existing.isDead())) {
KUID nodeId = node.getNodeID();
KUID furthestId = furthest.getNodeID();
if (nodeId.isNearerTo(valueId, furthestId)) {
// sb.append("CONDITION C").append("\n");
// sb.append("ME:").append(context.getLocalNode()).append("\n");
// sb.append("VALUE:").append(valueId).append("\n");
// sb.append("NODE:").append(node).append("\n");
// sb.append(CollectionUtils.toString(nodes)).append("\n");
// System.out.println(sb.toString());
return Operation.DELETE;
}
}
return Operation.NOTHING;
}
}