package kademlia.routing; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.TreeSet; import kademlia.KadConfiguration; import kademlia.node.Node; /** * A bucket in the Kademlia routing table * * @author Joshua Kissoon * @created 20140215 */ public class JKademliaBucket implements KademliaBucket { /* How deep is this bucket in the Routing Table */ private final int depth; /* Contacts stored in this routing table */ private final TreeSet<Contact> contacts; /* A set of last seen contacts that can replace any current contact that is unresponsive */ private final TreeSet<Contact> replacementCache; private final KadConfiguration config; { contacts = new TreeSet<>(); replacementCache = new TreeSet<>(); } /** * @param depth How deep in the routing tree is this bucket * @param config */ public JKademliaBucket(int depth, KadConfiguration config) { this.depth = depth; this.config = config; } @Override public synchronized void insert(Contact c) { if (this.contacts.contains(c)) { /** * If the contact is already in the bucket, lets update that we've seen it * We need to remove and re-add the contact to get the Sorted Set to update sort order */ Contact tmp = this.removeFromContacts(c.getNode()); tmp.setSeenNow(); tmp.resetStaleCount(); this.contacts.add(tmp); } else { /* If the bucket is filled, so put the contacts in the replacement cache */ if (contacts.size() >= this.config.k()) { /* If the cache is empty, we check if any contacts are stale and replace the stalest one */ Contact stalest = null; for (Contact tmp : this.contacts) { if (tmp.staleCount() >= this.config.stale()) { /* Contact is stale */ if (stalest == null) { stalest = tmp; } else if (tmp.staleCount() > stalest.staleCount()) { stalest = tmp; } } } /* If we have a stale contact, remove it and add the new contact to the bucket */ if (stalest != null) { this.contacts.remove(stalest); this.contacts.add(c); } else { /* No stale contact, lets insert this into replacement cache */ this.insertIntoReplacementCache(c); } } else { this.contacts.add(c); } } } @Override public synchronized void insert(Node n) { this.insert(new Contact(n)); } @Override public synchronized boolean containsContact(Contact c) { return this.contacts.contains(c); } @Override public synchronized boolean containsNode(Node n) { return this.containsContact(new Contact(n)); } @Override public synchronized boolean removeContact(Contact c) { /* If the contact does not exist, then we failed to remove it */ if (!this.contacts.contains(c)) { return false; } /* Contact exist, lets remove it only if our replacement cache has a replacement */ if (!this.replacementCache.isEmpty()) { /* Replace the contact with one from the replacement cache */ this.contacts.remove(c); Contact replacement = this.replacementCache.first(); this.contacts.add(replacement); this.replacementCache.remove(replacement); } else { /* There is no replacement, just increment the contact's stale count */ this.getFromContacts(c.getNode()).incrementStaleCount(); } return true; } private synchronized Contact getFromContacts(Node n) { for (Contact c : this.contacts) { if (c.getNode().equals(n)) { return c; } } /* This contact does not exist */ throw new NoSuchElementException("The contact does not exist in the contacts list."); } private synchronized Contact removeFromContacts(Node n) { for (Contact c : this.contacts) { if (c.getNode().equals(n)) { this.contacts.remove(c); return c; } } /* We got here means this element does not exist */ throw new NoSuchElementException("Node does not exist in the replacement cache. "); } @Override public synchronized boolean removeNode(Node n) { return this.removeContact(new Contact(n)); } @Override public synchronized int numContacts() { return this.contacts.size(); } @Override public synchronized int getDepth() { return this.depth; } @Override public synchronized List<Contact> getContacts() { final ArrayList<Contact> ret = new ArrayList<>(); /* If we have no contacts, return the blank arraylist */ if (this.contacts.isEmpty()) { return ret; } /* We have contacts, lets copy put them into the arraylist and return */ for (Contact c : this.contacts) { ret.add(c); } return ret; } /** * When the bucket is filled, we keep extra contacts in the replacement cache. */ private synchronized void insertIntoReplacementCache(Contact c) { /* Just return if this contact is already in our replacement cache */ if (this.replacementCache.contains(c)) { /** * If the contact is already in the bucket, lets update that we've seen it * We need to remove and re-add the contact to get the Sorted Set to update sort order */ Contact tmp = this.removeFromReplacementCache(c.getNode()); tmp.setSeenNow(); this.replacementCache.add(tmp); } else if (this.replacementCache.size() > this.config.k()) { /* if our cache is filled, we remove the least recently seen contact */ this.replacementCache.remove(this.replacementCache.last()); this.replacementCache.add(c); } else { this.replacementCache.add(c); } } private synchronized Contact removeFromReplacementCache(Node n) { for (Contact c : this.replacementCache) { if (c.getNode().equals(n)) { this.replacementCache.remove(c); return c; } } /* We got here means this element does not exist */ throw new NoSuchElementException("Node does not exist in the replacement cache. "); } @Override public synchronized String toString() { StringBuilder sb = new StringBuilder("Bucket at depth: "); sb.append(this.depth); sb.append("\n Nodes: \n"); for (Contact n : this.contacts) { sb.append("Node: "); sb.append(n.getNode().getNodeId().toString()); sb.append(" (stale: "); sb.append(n.staleCount()); sb.append(")"); sb.append("\n"); } return sb.toString(); } }