Java tutorial
/* * Copyright (c) 2015, Kasra Faghihi, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.offbynull.voip.kademlia.model; import com.offbynull.voip.kademlia.model.RouteTreeBucketStrategy.BucketParameters; import java.time.Instant; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.TreeSet; import org.apache.commons.lang3.Validate; /** * An implementation of Kademlia's route tree. This is an implementation of a <b>strict</b> route tree, meaning that it doesn't perform * the irregularly bucket splitting mentioned in the original Kademlia paper. The portion of the paper that deals with irregular bucket * splitting was never fully understood (discussion on topic can be found here: http://stackoverflow.com/q/32129978/1196226). * <p> * A strict route tree is a route tree that is static (doesn't split k-buckets after creation) and extends all the way done to your own * ID. For example, the route tree of node 000 would look something like this (assuming that the route tree branches only 1 bit at a * time)... * <pre> * 0/\1 * / [1xx BUCKET] * 0/\1 * / [01x BUCKET] * 0/\1 * [SELF] [001 BUCKET] * </pre> * @author Kasra Faghihi */ public final class RouteTree { private final Id baseId; private final RouteTreeNode root; private final TimeSet<BitString> bucketUpdateTimes; // prefix to when the prefix's bucket was last updated (not cache) private Instant lastTouchTime; /** * Construct a {@link RouteTree} object. * @param baseId ID of the node that this route tree is for * @param branchStrategy branching strategy (dictates how many branches to create at each depth) * @param bucketStrategy bucket strategy (dictates k-bucket parameters for each k-bucket) * @throws NullPointerException if any argument is {@code null} * @throws IllegalStateException if either {@code branchStrategy} or {@code bucketStrategy} generates invalid data (see interfaces for * restrictions) */ public RouteTree(Id baseId, // because id's are always > 0 in size -- it isn't possible for tree creation to mess up RouteTreeBranchStrategy branchStrategy, RouteTreeBucketStrategy bucketStrategy) { Validate.notNull(baseId); Validate.notNull(branchStrategy); Validate.notNull(bucketStrategy); this.baseId = baseId; // must be set before creating RouteTreeLevels this.bucketUpdateTimes = new TimeSet<>(); root = createRoot(branchStrategy, bucketStrategy); RouteTreeNode child = root; while (child != null) { child = growParent(child, branchStrategy, bucketStrategy); } // Special case: the routing tree has a bucket for baseId. Nothing can ever access that bucket (calls to // touch/stale/find with your own ID will result an exception) and it'll always be empty, so remove it from bucketUpdateTimes. bucketUpdateTimes.remove(baseId.getBitString()); this.lastTouchTime = Instant.MIN; } /** * Searches this route tree for the closest nodes to some ID. Node closeness is determined by the XOR metric -- Kademlia's notion of * distance. * <p> * Note this method will never return yourself (the node that this routing table is for). * @param id ID to search for * @param max maximum number of results to give back * @param includeStale if {@code true}, includes stale nodes in the results * @return up to {@code max} closest nodes to {@code id} (less are returned if this route table contains less than {@code max} nodes) * @throws NullPointerException if any argument is {@code null} * @throws IllegalArgumentException if any numeric argument is negative * @throws IdLengthMismatchException if the bitlength of {@code id} doesn't match the bitlength of the ID that this route tree is for * (the ID of the node this route tree belongs to) */ public List<Activity> find(Id id, int max, boolean includeStale) { Validate.notNull(id); InternalValidate.matchesLength(baseId.getBitLength(), id); // InternalValidate.notMatchesBase(baseId, id); // commented out because you should be able to search for closest nodes to yourself Validate.isTrue(max >= 0); // why would anyone want 0? let thru anyways IdXorMetricComparator comparator = new IdXorMetricComparator(id); TreeSet<Activity> output = new TreeSet<>( (x, y) -> comparator.compare(x.getNode().getId(), y.getNode().getId())); root.findNodesWithLargestPossiblePrefix(id, output, max, includeStale); return new ArrayList<>(output); } // used for testing List<Activity> dumpBucket(BitString prefix) { Validate.notNull(prefix); Validate.isTrue(prefix.getBitLength() < baseId.getBitLength()); // cannot be == or > KBucket bucket = root.getBucketForPrefix(prefix); return bucket.dumpBucket(true, true, false); } /** * Get all k-bucket prefixes in this route tree. * @return all k-bucket prefixes in this route tree */ public List<BitString> dumpBucketPrefixes() { List<BitString> output = new LinkedList<>(); root.dumpAllBucketPrefixes(output); return new ArrayList<>(output); } /** * Updates the appropriate k-bucket in this route tree by touching it. When the Kademlia node that this route tree is for receives a * request or response from some other node in the network, this method should be called. * <p> * See {@link KBucket#touch(java.time.Instant, com.offbynull.voip.kademlia.model.Node) } for more information. * @param time time which request or response came in * @param node node which issued the request or response * @return changes to collection of stored nodes and replacement cache of the k-bucket effected * @throws NullPointerException if any argument is {@code null} * @throws IdLengthMismatchException if the bitlength of {@code node}'s ID doesn't match the bitlength of the owning node's ID (the ID * of the node this route tree is for) * @throws BaseIdMatchException if {@code node}'s ID is the same as the owning node's ID (the ID of the node this route tree is for) * @throws BackwardTimeException if {@code time} is less than the time used in the previous invocation of this method * @throws LinkMismatchException if this route tree already contains a node with {@code node}'s ID but with a different link (SPECIAL * CASE: If the contained node is marked as stale, this exception will not be thrown. Since the node is marked as stale, it means it * should have been replaced but the replacement cache was empty. As such, this case is treated as if this were a new node replacing * a stale node, not a stale node being reverted to normal status -- the fact that the IDs are the same but the links don't match * doesn't matter) * @see KBucket#touch(java.time.Instant, com.offbynull.voip.kademlia.model.Node) */ public RouteTreeChangeSet touch(Instant time, Node node) { Validate.notNull(time); Validate.notNull(node); Id id = node.getId(); InternalValidate.matchesLength(baseId.getBitLength(), id); InternalValidate.notMatchesBase(baseId, id); InternalValidate.forwardTime(lastTouchTime, time); // time must be >= lastUpdatedTime lastTouchTime = time; KBucket bucket = root.getBucketFor(node.getId()); // because we use this method to find the appropriate kbucket, // IdPrefixMismatchException never occurs KBucketChangeSet kBucketChangeSet = bucket.touch(time, node); BitString kBucketPrefix = bucket.getPrefix(); // insert last bucket activity time in to bucket update times... it may be null if bucket has never been accessed, in which case // we insert MIN instead Instant lastBucketActivityTime = bucket.getLatestBucketActivityTime(); if (lastBucketActivityTime == null) { lastBucketActivityTime = Instant.MIN; } bucketUpdateTimes.remove(kBucketPrefix); bucketUpdateTimes.insert(lastBucketActivityTime, kBucketPrefix); return new RouteTreeChangeSet(kBucketPrefix, kBucketChangeSet); } /** * Marks a node within this route tree as stale (meaning that you're no longer able to communicate with it), evicting it and replacing * it with the most recent node in the effected k-bucket's replacement cache. * <p> * See {@link KBucket#stale(com.offbynull.voip.kademlia.model.Node) } for more information. * @param node node to mark as stale * @return changes to collection of stored nodes and replacement cache of the k-bucket effected * @throws NullPointerException if any argument is {@code null} * @throws IdLengthMismatchException if the bitlength of {@code node}'s ID doesn't match the bitlength of the owning node's ID (the ID * of the node this route tree is for) * @throws BaseIdMatchException if {@code node}'s ID is the same as the owning node's ID (the ID of the node this route tree is for) * @throws NodeNotFoundException if this route tree doesn't contain {@code node} * @throws LinkMismatchException if this route tree contains a node with {@code node}'s ID but with a different link * @throws BadNodeStateException if this route tree contains {@code node} but {@code node} is marked as locked * @see KBucket#stale(com.offbynull.voip.kademlia.model.Node) */ public RouteTreeChangeSet stale(Node node) { Validate.notNull(node); Id id = node.getId(); InternalValidate.matchesLength(baseId.getBitLength(), id); InternalValidate.notMatchesBase(baseId, id); KBucket bucket = root.getBucketFor(node.getId()); // because we use this method to find the appropriate kbucket, // IdPrefixMismatchException never occurs KBucketChangeSet kBucketChangeSet = bucket.stale(node); BitString kBucketPrefix = bucket.getPrefix(); // insert last bucket activity time in to bucket update times... it may be null if bucket has never been accessed, in which // case we insert MIN instead // // note that marking a node as stale may have replaced it in the bucket with another node in the cache. That cache node // could have an older time than the stale node, meaning that bucketUpdateTimes may actually be older after the replacement! Instant lastBucketActivityTime = bucket.getLatestBucketActivityTime(); if (lastBucketActivityTime == null) { lastBucketActivityTime = Instant.MIN; } bucketUpdateTimes.remove(kBucketPrefix); bucketUpdateTimes.insert(lastBucketActivityTime, kBucketPrefix); return new RouteTreeChangeSet(kBucketPrefix, kBucketChangeSet); } // Disable for now -- not being used // public void lock(Node node) { // Validate.notNull(node); // // Id id = node.getId(); // InternalValidate.matchesLength(baseId.getBitLength(), id); // InternalValidate.notMatchesBase(baseId, id); // // root.getBucketFor(node.getId()).lock(node); // } // // public void unlock(Node node) { // Validate.notNull(node); // // Id id = node.getId(); // InternalValidate.matchesLength(baseId.getBitLength(), id); // InternalValidate.notMatchesBase(baseId, id); // // root.getBucketFor(node.getId()).unlock(node); // } /** * Get prefixes for k-buckets that haven't been updated * (from {@link #touch(java.time.Instant, com.offbynull.voip.kademlia.model.Node) }) since the time specified. * @param time last update time threshold (k-buckets with their last update time before this get returned by this method) * @return prefixes for stagnant k-buckets * @throws NullPointerException if any argument is {@code null} */ public List<BitString> getStagnantBuckets(Instant time) { // is inclusive Validate.notNull(time); List<BitString> prefixes = bucketUpdateTimes.getBefore(time, true); return prefixes; } private static final BitString EMPTY = BitString.createFromString(""); private RouteTreeNode createRoot(RouteTreeBranchStrategy branchStrategy, RouteTreeBucketStrategy bucketStrategy) { Validate.notNull(branchStrategy); Validate.notNull(bucketStrategy); // Get number of branches/buckets to create for root int numOfBuckets = branchStrategy.getBranchCount(EMPTY); Validate.isTrue(numOfBuckets >= 0, "Branch cannot be negative, was %d", numOfBuckets); Validate.isTrue(numOfBuckets != 0, "Root of tree must contain at least 1 branch, was %d", numOfBuckets); Validate.isTrue(Integer.bitCount(numOfBuckets) == 1, "Branch count must be power of 2"); int suffixBitCount = Integer.bitCount(numOfBuckets - 1); // num of bits e.g. 8 --> 1000 - 1 = 0111, bitcount(0111) = 3 Validate.isTrue(suffixBitCount <= baseId.getBitLength(), "Attempting to branch too far (in root) %d bits extends past %d bits", suffixBitCount, baseId.getBitLength()); // Create buckets by creating a 0-sized top bucket and splitting it + resizing each split KBucket[] newBuckets = new KBucket(baseId, EMPTY, 0, 0).split(suffixBitCount); for (int i = 0; i < newBuckets.length; i++) { BucketParameters bucketParams = bucketStrategy.getBucketParameters(newBuckets[i].getPrefix()); int bucketSize = bucketParams.getBucketSize(); int cacheSize = bucketParams.getCacheSize(); newBuckets[i].resizeBucket(bucketSize); newBuckets[i].resizeCache(cacheSize); // insert last bucket activity time in to bucket update times... it may be null if bucket has never been accessed, in which case // we insert MIN instead Instant lastBucketActivityTime = newBuckets[i].getLatestBucketActivityTime(); if (lastBucketActivityTime == null) { lastBucketActivityTime = Instant.MIN; } bucketUpdateTimes.insert(lastBucketActivityTime, newBuckets[i].getPrefix()); } // Create root return new RouteTreeNode(EMPTY, suffixBitCount, newBuckets); } private RouteTreeNode growParent(RouteTreeNode parent, RouteTreeBranchStrategy branchStrategy, RouteTreeBucketStrategy bucketStrategy) { Validate.notNull(parent); Validate.notNull(branchStrategy); Validate.notNull(bucketStrategy); // Calculate which bucket from parent to split int parentNumOfBuckets = parent.getBranchCount(); Validate.validState(Integer.bitCount(parentNumOfBuckets) == 1); // sanity check numofbuckets is pow of 2 int parentPrefixBitLen = parent.getPrefix().getBitLength(); // num of bits in parent's prefix int parentSuffixBitCount = Integer.bitCount(parentNumOfBuckets - 1); // num of bits in parent's suffix // e.g. 8 --> 1000 - 1 = 0111, bitcount(0111) = 3 if (parentPrefixBitLen + parentSuffixBitCount >= baseId.getBitLength()) { // should never be >, only ==, but just in case // The parents prefix length + the number of bits the parent used for buckets > baseId's length. As such, it isn't possible to // grow any further, so don't even try. return null; } int splitBucketIdx = (int) baseId.getBitString().getBitsAsLong(parentPrefixBitLen, parentSuffixBitCount); KBucket splitBucket = parent.getBranch(splitBucketIdx).getItem(); BitString splitBucketPrefix = splitBucket.getPrefix(); // Get number of buckets to create for new level int numOfBuckets = branchStrategy.getBranchCount(splitBucketPrefix); Validate.isTrue(numOfBuckets >= 2, "Branch count must be atleast 2, was %d", numOfBuckets); Validate.isTrue(Integer.bitCount(numOfBuckets) == 1, "Branch count must be power of 2"); int suffixBitCount = Integer.bitCount(numOfBuckets - 1); // num of bits e.g. 8 (1000) -- 1000 - 1 = 0111, bitcount(0111) = 3 Validate.isTrue(splitBucketPrefix.getBitLength() + suffixBitCount <= baseId.getBitLength(), "Attempting to branch too far %s with %d bits extends past %d bits", splitBucketPrefix, suffixBitCount, baseId.getBitLength()); // Split parent bucket at that branch index BitString newPrefix = baseId.getBitString().getBits(0, parentPrefixBitLen + suffixBitCount); KBucket[] newBuckets = splitBucket.split(suffixBitCount); for (int i = 0; i < newBuckets.length; i++) { BucketParameters bucketParams = bucketStrategy.getBucketParameters(newBuckets[i].getPrefix()); int bucketSize = bucketParams.getBucketSize(); int cacheSize = bucketParams.getCacheSize(); newBuckets[i].resizeBucket(bucketSize); newBuckets[i].resizeCache(cacheSize); Instant lastBucketActivityTime = newBuckets[i].getLatestBucketActivityTime(); if (lastBucketActivityTime == null) { lastBucketActivityTime = Instant.MIN; } bucketUpdateTimes.insert(lastBucketActivityTime, newBuckets[i].getPrefix()); } // Get rid of parent bucket we just split. It branches down at that point, and any nodes that were contained within will be in the // newly created buckets bucketUpdateTimes.remove(splitBucketPrefix); // Create new level and set as child RouteTreeNode newNode = new RouteTreeNode(newPrefix, suffixBitCount, newBuckets); parent.setBranch(splitBucketIdx, new RouteTreeNodeBranch(newNode)); return newNode; } }