kademlia-1.1.0.0: src/Network/Kademlia/Tree.hs
{-|
Module : Network.Kademlia.Tree
Description : Implementation of the Node Storage Tree
Network.Kademlia.Tree implements the Node Storage Tree used to store
and look up the known nodes.
This module is designed to be used as a qualified import.
-}
module Network.Kademlia.Tree
( NodeTree
, create
, insert
, lookup
, delete
, handleTimeout
, findClosest
, extractId
, toList
, fold
) where
import Prelude hiding (lookup)
import Network.Kademlia.Types
import qualified Data.List as L (find, delete)
data NodeTree i = NodeTree ByteStruct (NodeTreeElem i)
data NodeTreeElem i = Split (NodeTreeElem i) (NodeTreeElem i)
| Bucket ([(Node i, Int)], [Node i])
type NodeTreeFunction i a = Int -> Bool -> ([(Node i, Int)], [Node i]) -> a
-- | Modify the position in the tree where the supplied id would be
modifyAt :: (Serialize i) =>
NodeTree i -> i -> NodeTreeFunction i (NodeTreeElem i)
-> NodeTree i
modifyAt (NodeTree idStruct elem) id f =
let targetStruct = toByteStruct id
newElems = go idStruct targetStruct 0 True elem
in NodeTree idStruct newElems
where -- This function is partial, but we know that there will alwasys be a
-- bucket at the end. Therefore, we don't have to check for empty
-- ByteStructs
--
-- Apply the function to the position of the bucket
go _ _ depth valid (Bucket b) = f depth valid b
-- If the bit is a 0, go left
go (i:is) (False:ts) depth valid (Split left right) =
let new = go is ts (depth + 1) (valid && not i) left
in Split new right
-- Otherwise, continue to the right
go (i:is) (True:ts) depth valid (Split left right) =
let new = go is ts (depth + 1) (valid && i) right
in Split left new
-- | Modify and apply a function at the position in the tree where the
-- supplied id would be
bothAt :: (Serialize i) =>
NodeTree i -> i -> NodeTreeFunction i (NodeTreeElem i, a)
-> (NodeTree i, a)
bothAt (NodeTree idStruct elem) id f =
let targetStruct = toByteStruct id
(newElems, val) = go idStruct targetStruct 0 True elem
in (NodeTree idStruct newElems, val)
where -- This function is partial, but we know that there will alwasys be a
-- bucket at the end. Therefore, we don't have to check for empty
-- ByteStructs
--
-- Apply the function to the position of the bucket
go _ _ depth valid (Bucket b) = f depth valid b
-- If the bit is a 0, go left
go (i:is) (False:ts) depth valid (Split left right) =
let (new, val) = go is ts (depth + 1) (valid && not i) left
in (Split new right, val)
-- Otherwise, continue to the right
go (i:is) (True:ts) depth valid (Split left right) =
let (new, val) = go is ts (depth + 1) (valid && i) right
in (Split left new, val)
-- | Apply a function to the bucket the supplied id would be located in
applyAt :: (Serialize i) => NodeTree i -> i -> NodeTreeFunction i a -> a
applyAt (NodeTree idStruct elem) id f =
let targetStruct = toByteStruct id
in go idStruct targetStruct 0 True elem
where -- This function is partial for the same reason as in modifyAt
--
-- Apply the function
go _ _ depth valid (Bucket b) = f depth valid b
-- If the bit is a 0, go left
go (i:is) (False:ts) depth valid (Split left _) =
go is ts (depth + 1) (valid && not i) left
-- Otherwise, continue to the right
go (i:is) (True:ts) depth valid (Split _ right) =
go is ts (depth + 1) (valid && i) right
-- | Create a NodeTree corresponding to the id
create :: (Serialize i) => i -> NodeTree i
create id = NodeTree (toByteStruct id) . Bucket $ ([], [])
-- | Lookup a node within a NodeTree
lookup :: (Serialize i, Eq i) => NodeTree i -> i -> Maybe (Node i)
lookup tree id = applyAt tree id f
where f _ _ = L.find (idMatches id) . map fst . fst
-- | Delete a Node corresponding to a supplied Id from a NodeTree
delete :: (Serialize i, Eq i) => NodeTree i -> i -> NodeTree i
delete tree id = modifyAt tree id f
where f _ _ (nodes, cache) =
let deleted = filter (not . idMatches id . fst) $ nodes
in Bucket (deleted, cache)
-- | Handle a timed out node by incrementing its timeoutCount and deleting it
-- if the count exceeds the limit. Also, return wether it's reasonable to ping
-- the node again.
handleTimeout :: (Serialize i, Eq i) => NodeTree i -> i -> (NodeTree i, Bool)
handleTimeout tree id = bothAt tree id f
where f _ _ (nodes, cache) = case L.find (idMatches id . fst) nodes of
-- Delete a node that exceeded the limit. Don't contact it again
-- as it is now considered dead
Just x@(_, 4) -> (Bucket (L.delete x $ nodes, cache), False)
-- Increment the timeoutCount
Just x@(n, timeoutCount) ->
(Bucket ((n, timeoutCount + 1) : L.delete x nodes, cache), True)
-- Don't contact an unknown node a second time
Nothing -> (Bucket (nodes, cache), False)
-- | Refresh the node corresponding to a supplied Id by placing it at the first
-- index of it's KBucket and reseting its timeoutCount, then return a Bucket
-- NodeTreeElem
refresh :: (Serialize i, Eq i) => Node i -> ([(Node i, Int)], [Node i]) -> NodeTreeElem i
refresh node (nodes, cache) =
Bucket (case L.find (idMatches (nodeId node) . fst) nodes of
Just x@(n, _) -> (n, 0) : L.delete x nodes
_ -> nodes
, cache)
-- | Insert a node into a NodeTree
insert :: (Serialize i, Eq i) => NodeTree i -> Node i -> NodeTree i
insert tree node = if applyAt tree (nodeId node) needsSplit
-- Split the tree before inserting, when it makes sense
then let splitTree = split tree . nodeId $ node
in insert splitTree node
-- Insert the node
else modifyAt tree (nodeId node) doInsert
where needsSplit depth valid (nodes, _) =
let maxDepth = (length . toByteStruct . nodeId $ node) - 1
in -- A new node will be inserted
node `notElem` map fst nodes &&
-- The bucket is full
length nodes >= 7 &&
-- The bucket may be split
(depth < 5 || valid) && depth <= maxDepth
doInsert _ _ b@(nodes, cache)
-- Refresh an already existing node
| node `elem` map fst nodes = refresh node b
-- Simply insert the node, if the bucket isn't full
| length nodes < 7 = Bucket ((node, 0):nodes, cache)
-- Move the node to the first spot, if it's already cached
| node `elem` cache = Bucket (nodes, node : L.delete node cache)
-- Cache the node and drop older ones, if necessary
| otherwise = Bucket (nodes, node : take 4 cache)
-- | Split the KBucket the specified id would reside in into two and return a
-- Split NodeTreeElem
split :: (Serialize i) => NodeTree i -> i -> NodeTree i
split tree splitId = modifyAt tree splitId f
where f depth _ (nodes, cache) =
let (leftNodes, rightNodes) = splitBucket depth fst nodes
(leftCache, rightCache) = splitBucket depth id cache
in Split
(Bucket (leftNodes, leftCache))
(Bucket (rightNodes, rightCache))
-- Recursivly split the nodes into two buckets
splitBucket _ _ [] = ([], [])
splitBucket i f (n:ns) = let bs = toByteStruct . nodeId . f $ n
bit = bs !! i
(left, right) = splitBucket i f ns
in if bit
then (left, n:right)
else (n:left, right)
-- | Find the k closest Nodes to a given Id
findClosest :: (Serialize i) => NodeTree i -> i -> Int -> [Node i]
findClosest (NodeTree idStruct elem) id n =
let targetStruct = toByteStruct id
in go idStruct targetStruct elem n
where -- This function is partial for the same reason as in modifyAt
--
-- Take the n closest nodes
go _ _ (Bucket (nodes, _)) n
| length nodes <= n = map fst nodes
| otherwise = take n . sortByDistanceTo (map fst nodes) $ id
-- Take the closest nodes from the left child first, if those aren't
-- enough, take the rest from the right
go (i:is) (False:ts) (Split left right) n =
let result = go is ts left n
in if length result == n
then result
else result ++ go is ts right n
-- Take the closest nodes from the right child first, if those aren't
-- enough, take the rest from the left
go (i:is) (True:ts) (Split left right) n =
let result = go is ts right n
in if length result == n
then result
else result ++ go is ts left n
-- Extract original Id from NodeTree
extractId :: (Serialize i) => NodeTree i -> i
extractId (NodeTree id _) = fromByteStruct id
-- | Helper function used for KBucket manipulation
idMatches :: (Eq i) => i -> Node i -> Bool
idMatches id node = id == nodeId node
-- | Turn the NodeTree into a list of nodes
toList :: NodeTree i -> [Node i]
toList (NodeTree _ elems) = go elems
where go (Split left right) = go left ++ go right
go (Bucket b) = map fst . fst $ b
-- | Fold over the buckets
fold :: ([Node i] -> a -> a) -> a -> NodeTree i -> a
fold f init (NodeTree _ elems) = go init elems
where go a (Split left right) = let a' = go a left in go a' right
go a (Bucket b) = f (map fst . fst $ b) a