/* * 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.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import org.limewire.collection.PatriciaTrie; import org.limewire.collection.TrieUtils; import org.limewire.collection.Trie.Cursor; import org.limewire.mojito.KUID; import org.limewire.mojito.routing.Bucket; import org.limewire.mojito.routing.ClassfulNetworkCounter; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.routing.RouteTable; import org.limewire.mojito.settings.KademliaSettings; import org.limewire.mojito.settings.RouteTableSettings; import org.limewire.mojito.util.FixedSizeHashMap; /** * An implementation of Bucket */ class BucketNode implements Bucket { private static final long serialVersionUID = -4116522147032657308L; private final RouteTable routeTable; private final KUID bucketId; private final int depth; private final PatriciaTrie<KUID, Contact> nodeTrie; private transient Map<KUID, Contact> cache; private transient ClassfulNetworkCounter counter; private long timeStamp = 0L; public BucketNode(RouteTable routeTable, KUID bucketId, int depth) { this.routeTable = routeTable; this.bucketId = bucketId; this.depth = depth; nodeTrie = new PatriciaTrie<KUID, Contact>(KUID.KEY_ANALYZER); init(); } private void init() { cache = Collections.emptyMap(); counter = new ClassfulNetworkCounter(this); } void postInit() { for (Contact node : nodeTrie.values()) { counter.incrementAndGet(node); } } public KUID getBucketID() { return bucketId; } public RouteTable getRouteTable() { return routeTable; } public boolean isLocalNode(Contact node) { return routeTable.isLocalNode(node); } public ClassfulNetworkCounter getClassfulNetworkCounter() { return counter; } public int getDepth() { return depth; } public void touch() { timeStamp = System.currentTimeMillis(); } public long getTimeStamp() { return timeStamp; } public void addActiveContact(Contact node) { checkNodeID(node); assert (isActiveFull() == false); Contact existing = nodeTrie.put(node.getNodeID(), node); assert (existing == null); if(node.isAlive()) { touch(); } counter.incrementAndGet(node); } public Contact addCachedContact(Contact node) { checkNodeID(node); if (cache == Collections.EMPTY_MAP) { int maxSize = RouteTableSettings.MAX_CACHE_SIZE.getValue(); cache = new FixedSizeHashMap<KUID, Contact>(maxSize/2, 0.75f, true, maxSize); } if (!isCacheFull()) { Contact existing = cache.put(node.getNodeID(), node); assert (existing == null); } else { Contact lrs = getLeastRecentlySeenCachedContact(); if (!lrs.isAlive() || (!lrs.hasBeenRecentlyAlive() && node.isAlive())) { Contact c = cache.remove(lrs.getNodeID()); assert (c == lrs); cache.put(node.getNodeID(), node); return c; } } return null; } public Contact updateContact(Contact node) { checkNodeID(node); KUID nodeId = node.getNodeID(); if (containsActiveContact(nodeId)) { Contact current = nodeTrie.put(nodeId, node); assert (current != null); // Remove the old Network counter.decrementAndGet(current); // And add the new Network counter.incrementAndGet(node); return current; } else if (containsCachedContact(nodeId)) { return cache.put(nodeId, node); } throw new IllegalStateException(node + " is not in this Bucket " + toString()); } // TODO: Disable/Delete when finished with testing! private void checkNodeID(Contact node) { if (depth <= 0) { // This is the ROOT Bucket! return; } int bitIndex = bucketId.bitIndex(node.getNodeID()); if (bitIndex < 0) { return; } assert (bitIndex >= depth) : "Wrong Bucket"; } public Contact get(KUID nodeId) { Contact node = getActiveContact(nodeId); if (node == null) { node = getCachedContact(nodeId); } return node; } public Contact getActiveContact(KUID nodeId) { return nodeTrie.get(nodeId); } public Contact getCachedContact(KUID nodeId) { return cache.get(nodeId); } public Contact select(KUID nodeId) { return nodeTrie.select(nodeId); } public Collection<Contact> select(KUID nodeId, int count) { return TrieUtils.select(nodeTrie, nodeId, count); } public boolean remove(KUID nodeId) { if (removeActiveContact(nodeId)) { return true; } else { return removeCachedContact(nodeId); } } public boolean removeActiveContact(KUID nodeId) { Contact node = nodeTrie.remove(nodeId); if (node != null) { int old = counter.get(node); int now = counter.decrementAndGet(node); assert (now < old) : now + " < " + old + ", " + nodeId + ", " + node + this; return true; } return false; } public boolean removeCachedContact(KUID nodeId) { if (cache.remove(nodeId) != null) { if (cache.isEmpty()) { cache = Collections.emptyMap(); } return true; } else { return false; } } public boolean contains(KUID nodeId) { if (containsActiveContact(nodeId)) { return true; } else { return containsCachedContact(nodeId); } } public boolean containsActiveContact(KUID nodeId) { return nodeTrie.containsKey(nodeId); } public boolean containsCachedContact(KUID nodeId) { return cache.containsKey(nodeId); } public boolean isActiveFull() { return nodeTrie.size() >= getMaxActiveSize(); } public boolean isCacheFull() { return !cache.isEmpty() && ((FixedSizeHashMap<KUID, Contact>)cache).isFull(); } public boolean isInSmallestSubtree() { int commonPrefixLength = routeTable.getLocalNode().getNodeID().getCommonPrefixLength(bucketId); int localNodeDepth = routeTable.getBucket(routeTable.getLocalNode().getNodeID()).getDepth(); return (localNodeDepth - 1) == commonPrefixLength; } public boolean isTooDeep() { int commonPrefixLength = routeTable.getLocalNode().getNodeID().getCommonPrefixLength(bucketId); return (depth - commonPrefixLength) >= RouteTableSettings.DEPTH_LIMIT.getValue(); } public Collection<Contact> getActiveContacts() { return nodeTrie.values(); } public Collection<Contact> getCachedContacts() { return cache.values(); } public Contact getLeastRecentlySeenActiveContact() { final Contact[] leastRecentlySeen = new Contact[]{ null }; nodeTrie.traverse(new Cursor<KUID, Contact>() { public SelectStatus select(Map.Entry<? extends KUID, ? extends Contact> entry) { Contact node = entry.getValue(); Contact lrs = leastRecentlySeen[0]; if (lrs == null || node.getTimeStamp() < lrs.getTimeStamp()) { leastRecentlySeen[0] = node; } return SelectStatus.CONTINUE; } }); return leastRecentlySeen[0]; } public Contact getMostRecentlySeenActiveContact() { final Contact[] mostRecentlySeen = new Contact[]{ null }; nodeTrie.traverse(new Cursor<KUID, Contact>() { public SelectStatus select(Map.Entry<? extends KUID, ? extends Contact> entry) { Contact node = entry.getValue(); Contact mrs = mostRecentlySeen[0]; if (mrs == null || node.getTimeStamp() > mrs.getTimeStamp()) { mostRecentlySeen[0] = node; } return SelectStatus.CONTINUE; } }); return mostRecentlySeen[0]; } public void purge() { for (Iterator<Contact> it = nodeTrie.values().iterator(); it.hasNext(); ) { Contact node = it.next(); if(!node.isAlive() && !isLocalNode(node)) { it.remove(); } } if(!isActiveFull() && !cache.isEmpty()) { // The cache Map is in LRS order. Add the Contacts to a List and // iterate it backwards so that we get the elements in MRS order. List<Contact> contacts = new ArrayList<Contact>(getCachedContacts()); for(int i = contacts.size()-1; i>=0 && !isActiveFull(); i--) { Contact node = contacts.get(i); if(node.isAlive()) { nodeTrie.put(node.getNodeID(), node); } boolean removed = removeCachedContact(node.getNodeID()); assert (removed); } } } // O(1) public Contact getLeastRecentlySeenCachedContact() { if (getCachedContacts().isEmpty()) { return null; } return getCachedContacts().iterator().next(); } // O(n) public Contact getMostRecentlySeenCachedContact() { Contact node = null; for(Contact n : getCachedContacts()) { node = n; } return node; } public List<Bucket> split() { assert (getCachedContacts().isEmpty() == true); Bucket left = new BucketNode(routeTable, bucketId, depth+1); Bucket right = new BucketNode(routeTable, bucketId.set(depth), depth+1); for (Contact node : getActiveContacts()) { KUID nodeId = node.getNodeID(); if (!nodeId.isBitSet(depth)) { left.addActiveContact(node); } else { right.addActiveContact(node); } } assert ((left.size() + right.size()) == size()) : "left: " + left + ", right: " + right + ", old: " + nodeTrie; return Arrays.asList(left, right); } public int size() { return getActiveSize() + getCacheSize(); } public int getActiveSize() { return nodeTrie.size(); } public int getMaxActiveSize() { return KademliaSettings.REPLICATION_PARAMETER.getValue(); } public int getCacheSize() { return cache.size(); } public boolean isRefreshRequired() { if ((System.currentTimeMillis() - getTimeStamp()) >= RouteTableSettings.BUCKET_REFRESH_PERIOD.getValue()) { return true; } return false; } public void clear() { nodeTrie.clear(); cache = Collections.emptyMap(); } @Override public int hashCode() { return bucketId.hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof BucketNode)) { return false; } BucketNode other = (BucketNode)o; return bucketId.equals(other.bucketId) && depth == other.depth; } @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(bucketId).append(" (depth=").append(getDepth()) .append(", active=").append(getActiveSize()) .append(", cache=").append(getCacheSize()).append(")\n"); Iterator<Contact> it = getActiveContacts().iterator(); for(int i = 0; it.hasNext(); i++) { buffer.append(" ").append(i).append(": ").append(it.next()).append("\n"); } if (!getCachedContacts().isEmpty()) { buffer.append("---\n"); it = getCachedContacts().iterator(); for(int i = 0; it.hasNext(); i++) { buffer.append(" ").append(i).append(": ").append(it.next()).append("\n"); } } return buffer.toString(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); init(); } }