forked from schmittlauch/Hash2Pub
638 lines
30 KiB
Haskell
638 lines
30 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DerivingStrategies #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{- |
|
|
Module : FediChord
|
|
Description : An opinionated implementation of the EpiChord DHT by Leong et al.
|
|
Copyright : (c) schmittlauch, 2019-2020
|
|
License : AGPL-3
|
|
Stability : experimental
|
|
|
|
Modernised EpiChord + k-choices load balancing
|
|
-}
|
|
|
|
module Hash2Pub.FediChord (
|
|
NodeID -- abstract, but newtype constructors cannot be hidden
|
|
, getNodeID
|
|
, toNodeID
|
|
, NodeState (..)
|
|
, LocalNodeState (..)
|
|
, RemoteNodeState (..)
|
|
, setSuccessors
|
|
, setPredecessors
|
|
, NodeCache
|
|
, CacheEntry(..)
|
|
, cacheGetNodeStateUnvalidated
|
|
, initCache
|
|
, cacheLookup
|
|
, cacheLookupSucc
|
|
, cacheLookupPred
|
|
, localCompare
|
|
, genNodeID
|
|
, genNodeIDBS
|
|
, genKeyID
|
|
, genKeyIDBS
|
|
, byteStringToUInteger
|
|
, ipAddrAsBS
|
|
, bsAsIpAddr
|
|
, FediChordConf(..)
|
|
, fediChordInit
|
|
, fediChordJoin
|
|
, fediChordBootstrapJoin
|
|
, tryBootstrapJoining
|
|
, fediMainThreads
|
|
, RealNode (..)
|
|
, nodeStateInit
|
|
, mkServerSocket
|
|
, mkSendSocket
|
|
, resolve
|
|
, cacheWriter
|
|
, joinOnNewEntriesThread
|
|
) where
|
|
|
|
import Control.Applicative ((<|>))
|
|
import Control.Concurrent
|
|
import Control.Concurrent.Async
|
|
import Control.Concurrent.STM
|
|
import Control.Concurrent.STM.TQueue
|
|
import Control.Concurrent.STM.TVar
|
|
import Control.Exception
|
|
import Control.Monad (forM_, forever)
|
|
import Control.Monad.Except
|
|
import Crypto.Hash
|
|
import qualified Data.ByteArray as BA
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.ByteString.UTF8 as BSU
|
|
import Data.Either (rights)
|
|
import Data.Foldable (foldr')
|
|
import Data.Functor.Identity
|
|
import Data.IP (IPv6, fromHostAddress6,
|
|
toHostAddress6)
|
|
import Data.List ((\\))
|
|
import qualified Data.Map.Strict as Map
|
|
import Data.Maybe (catMaybes, fromJust, fromMaybe,
|
|
isJust, isNothing, mapMaybe)
|
|
import qualified Data.Set as Set
|
|
import Data.Time.Clock.POSIX
|
|
import Data.Typeable (Typeable (..), typeOf)
|
|
import Data.Word
|
|
import qualified Network.ByteOrder as NetworkBytes
|
|
import Network.Socket hiding (recv, recvFrom, send,
|
|
sendTo)
|
|
import Network.Socket.ByteString
|
|
import Safe
|
|
import System.Random (randomRIO)
|
|
|
|
import Hash2Pub.DHTProtocol
|
|
import Hash2Pub.FediChordTypes
|
|
import Hash2Pub.Utils
|
|
|
|
import Debug.Trace (trace)
|
|
|
|
-- | initialise data structures, compute own IDs and bind to listening socket
|
|
-- ToDo: load persisted state, thus this function already operates in IO
|
|
fediChordInit :: FediChordConf -> IO (Socket, LocalNodeStateSTM)
|
|
fediChordInit initConf = do
|
|
let realNode = RealNode {
|
|
vservers = []
|
|
, nodeConfig = initConf
|
|
, bootstrapNodes = confBootstrapNodes initConf
|
|
}
|
|
realNodeSTM <- newTVarIO realNode
|
|
initialState <- nodeStateInit realNodeSTM
|
|
initialStateSTM <- newTVarIO initialState
|
|
serverSock <- mkServerSocket (getIpAddr initialState) (getDhtPort initialState)
|
|
pure (serverSock, initialStateSTM)
|
|
|
|
-- | initialises the 'NodeState' for this local node.
|
|
-- Separated from 'fediChordInit' to be usable in tests.
|
|
nodeStateInit :: RealNodeSTM -> IO LocalNodeState
|
|
nodeStateInit realNodeSTM = do
|
|
realNode <- readTVarIO realNodeSTM
|
|
cacheSTM <- newTVarIO initCache
|
|
q <- atomically newTQueue
|
|
let
|
|
conf = nodeConfig realNode
|
|
vsID = 0
|
|
containedState = RemoteNodeState {
|
|
domain = confDomain conf
|
|
, ipAddr = confIP conf
|
|
, nid = genNodeID (confIP conf) (confDomain conf) $ fromInteger vsID
|
|
, dhtPort = toEnum $ confDhtPort conf
|
|
, servicePort = 0
|
|
, vServerID = vsID
|
|
}
|
|
initialState = LocalNodeState {
|
|
nodeState = containedState
|
|
, nodeCacheSTM = cacheSTM
|
|
, cacheWriteQueue = q
|
|
, successors = []
|
|
, predecessors = []
|
|
, kNeighbours = 3
|
|
, lNumBestNodes = 3
|
|
, pNumParallelQueries = 2
|
|
, jEntriesPerSlice = 2
|
|
, parentRealNode = realNodeSTM
|
|
}
|
|
pure initialState
|
|
|
|
-- | Join a new node into the DHT, using a provided bootstrap node as initial cache seed
|
|
-- for resolving the new node's position.
|
|
fediChordBootstrapJoin :: LocalNodeStateSTM -- ^ the local 'NodeState'
|
|
-> (String, PortNumber) -- ^ domain and port of a bootstrapping node
|
|
-> IO (Either String LocalNodeStateSTM) -- ^ the joined 'NodeState' after a
|
|
-- successful join, otherwise an error message
|
|
fediChordBootstrapJoin nsSTM bootstrapNode = do
|
|
-- can be invoked multiple times with all known bootstrapping nodes until successfully joined
|
|
ns <- readTVarIO nsSTM
|
|
runExceptT $ do
|
|
-- 1. get routed to the currently responsible node
|
|
lookupResp <- liftIO $ bootstrapQueryId nsSTM bootstrapNode $ getNid ns
|
|
currentlyResponsible <- liftEither lookupResp
|
|
liftIO . putStrLn $ "Trying to join on " <> show (getNid currentlyResponsible)
|
|
-- 2. then send a join to the currently responsible node
|
|
joinResult <- liftIO $ requestJoin currentlyResponsible nsSTM
|
|
liftEither joinResult
|
|
|
|
-- Periodically lookup own ID through a random bootstrapping node to discover and merge separated DHT clusters.
|
|
-- Unjoined try joining instead.
|
|
convergenceSampleThread :: LocalNodeStateSTM -> IO ()
|
|
convergenceSampleThread nsSTM = forever $ do
|
|
nsSnap <- readTVarIO nsSTM
|
|
parentNode <- readTVarIO $ parentRealNode nsSnap
|
|
if isJoined nsSnap
|
|
then
|
|
runExceptT (do
|
|
-- joined node: choose random node, do queryIDLoop, compare result with own responsibility
|
|
let bss = bootstrapNodes parentNode
|
|
randIndex <- liftIO $ randomRIO (0, length bss - 1)
|
|
chosenNode <- maybe (throwError "invalid bootstrapping node index") pure $ atMay bss randIndex
|
|
lookupResult <- liftIO $ bootstrapQueryId nsSTM chosenNode (getNid nsSnap)
|
|
currentlyResponsible <- liftEither lookupResult
|
|
if getNid currentlyResponsible /= getNid nsSnap
|
|
-- if mismatch, stabilise on the result, else do nothing
|
|
then do
|
|
stabResult <- liftIO $ requestStabilise nsSnap currentlyResponsible
|
|
(preds, succs) <- liftEither stabResult
|
|
-- TODO: verify neighbours before adding, see #55
|
|
liftIO . atomically $ do
|
|
ns <- readTVar nsSTM
|
|
writeTVar nsSTM $ addPredecessors preds ns
|
|
else pure ()
|
|
) >> pure ()
|
|
-- unjoined node: try joining through all bootstrapping nodes
|
|
else tryBootstrapJoining nsSTM >> pure ()
|
|
let delaySecs = confBootstrapSamplingInterval . nodeConfig $ parentNode
|
|
threadDelay $ delaySecs * 10^6
|
|
|
|
|
|
-- | Try joining the DHT through any of the bootstrapping nodes until it succeeds.
|
|
tryBootstrapJoining :: LocalNodeStateSTM -> IO (Either String LocalNodeStateSTM)
|
|
tryBootstrapJoining nsSTM = do
|
|
bss <- atomically $ do
|
|
nsSnap <- readTVar nsSTM
|
|
realNodeSnap <- readTVar $ parentRealNode nsSnap
|
|
pure $ bootstrapNodes realNodeSnap
|
|
tryJoining bss
|
|
where
|
|
tryJoining (bn:bns) = do
|
|
j <- fediChordBootstrapJoin nsSTM bn
|
|
case j of
|
|
Left err -> putStrLn ("join error: " <> err) >> tryJoining bns
|
|
Right joined -> pure $ Right joined
|
|
tryJoining [] = pure $ Left "Exhausted all bootstrap points for joining."
|
|
|
|
|
|
-- | Look up a key just based on the responses of a single bootstrapping node.
|
|
bootstrapQueryId :: LocalNodeStateSTM -> (String, PortNumber) -> NodeID -> IO (Either String RemoteNodeState)
|
|
bootstrapQueryId nsSTM (bootstrapHost, bootstrapPort) targetID = do
|
|
ns <- readTVarIO nsSTM
|
|
srcAddr <- confIP . nodeConfig <$> readTVarIO (parentRealNode ns)
|
|
bootstrapResponse <- bracket (mkSendSocket srcAddr bootstrapHost bootstrapPort) close (
|
|
-- Initialise an empty cache only with the responses from a bootstrapping node
|
|
fmap Right . sendRequestTo 5000 3 (lookupMessage targetID ns Nothing)
|
|
)
|
|
`catch` (\e -> pure . Left $ "Error at bootstrap QueryId: " <> displayException (e :: IOException))
|
|
|
|
case bootstrapResponse of
|
|
Left err -> pure $ Left err
|
|
Right resp
|
|
| resp == Set.empty -> pure . Left $ "Bootstrapping node " <> show bootstrapHost <> " gave no response."
|
|
| otherwise -> do
|
|
now <- getPOSIXTime
|
|
-- create new cache with all returned node responses
|
|
let bootstrapCache =
|
|
-- traverse response parts
|
|
foldr' (\resp cacheAcc -> case queryResult <$> payload resp of
|
|
Nothing -> cacheAcc
|
|
Just (FOUND result1) -> addCacheEntryPure now (RemoteCacheEntry result1 now) cacheAcc
|
|
Just (FORWARD resultset) -> foldr' (addCacheEntryPure now) cacheAcc resultset
|
|
)
|
|
initCache resp
|
|
currentlyResponsible <- queryIdLookupLoop bootstrapCache ns 50 $ getNid ns
|
|
pure $ Right currentlyResponsible
|
|
|
|
|
|
-- | join a node to the DHT using the global node cache
|
|
-- node's position.
|
|
fediChordJoin :: LocalNodeStateSTM -- ^ the local 'NodeState'
|
|
-> IO (Either String LocalNodeStateSTM) -- ^ the joined 'NodeState' after a
|
|
-- successful join, otherwise an error message
|
|
fediChordJoin nsSTM = do
|
|
ns <- readTVarIO nsSTM
|
|
-- 1. get routed to the currently responsible node
|
|
currentlyResponsible <- requestQueryID ns $ getNid ns
|
|
putStrLn $ "Trying to join on " <> show (getNid currentlyResponsible)
|
|
-- 2. then send a join to the currently responsible node
|
|
joinResult <- requestJoin currentlyResponsible nsSTM
|
|
case joinResult of
|
|
Left err -> pure . Left $ "Error joining on " <> err
|
|
Right joinedNS -> pure . Right $ joinedNS
|
|
|
|
|
|
-- | Wait for new cache entries to appear and then try joining on them.
|
|
-- Exits after successful joining.
|
|
joinOnNewEntriesThread :: LocalNodeStateSTM -> IO ()
|
|
joinOnNewEntriesThread nsSTM = loop
|
|
where
|
|
loop = do
|
|
nsSnap <- readTVarIO nsSTM
|
|
(lookupResult, cache) <- atomically $ do
|
|
cache <- readTVar $ nodeCacheSTM nsSnap
|
|
case queryLocalCache nsSnap cache 1 (getNid nsSnap) of
|
|
-- empty cache, block until cache changes and then retry
|
|
(FORWARD s) | Set.null s -> retry
|
|
result -> pure (result, cache)
|
|
case lookupResult of
|
|
-- already joined
|
|
FOUND _ -> do
|
|
print =<< readTVarIO nsSTM
|
|
pure ()
|
|
-- otherwise try joining
|
|
FORWARD _ -> do
|
|
joinResult <- fediChordJoin nsSTM
|
|
either
|
|
-- on join failure, sleep and retry
|
|
-- TODO: make delay configurable
|
|
(const $ threadDelay (30 * 10^6) >> loop)
|
|
(const $ pure ())
|
|
joinResult
|
|
emptyset = Set.empty -- because pattern matches don't accept qualified names
|
|
|
|
|
|
-- | cache updater thread that waits for incoming NodeCache update instructions on
|
|
-- the node's cacheWriteQueue and then modifies the NodeCache as the single writer.
|
|
cacheWriter :: LocalNodeStateSTM -> IO ()
|
|
cacheWriter nsSTM =
|
|
forever $ atomically $ do
|
|
ns <- readTVar nsSTM
|
|
cacheModifier <- readTQueue $ cacheWriteQueue ns
|
|
modifyTVar' (nodeCacheSTM ns) cacheModifier
|
|
|
|
|
|
-- TODO: make max entry age configurable
|
|
maxEntryAge :: POSIXTime
|
|
maxEntryAge = 600
|
|
|
|
|
|
-- | Periodically iterate through cache, clean up expired entries and verify unverified ones
|
|
cacheVerifyThread :: LocalNodeStateSTM -> IO ()
|
|
cacheVerifyThread nsSTM = forever $ do
|
|
putStrLn "cache verify run: begin"
|
|
-- get cache
|
|
(ns, cache) <- atomically $ do
|
|
ns <- readTVar nsSTM
|
|
cache <- readTVar $ nodeCacheSTM ns
|
|
pure (ns, cache)
|
|
-- iterate entries:
|
|
-- for avoiding too many time syscalls, get current time before iterating.
|
|
now <- getPOSIXTime
|
|
forM_ (cacheEntries cache) (\(CacheEntry validated node ts) ->
|
|
-- case too old: delete (future work: decide whether pinging and resetting timestamp is better)
|
|
if (now - ts) > maxEntryAge
|
|
then
|
|
queueDeleteEntry (getNid node) ns
|
|
-- case unverified: try verifying, otherwise delete
|
|
else if not validated
|
|
then do
|
|
-- marking as verified is done by 'requestPing' as well
|
|
pong <- requestPing ns node
|
|
either (\_->
|
|
queueDeleteEntry (getNid node) ns
|
|
)
|
|
(\vss ->
|
|
if node `notElem` vss
|
|
then queueDeleteEntry (getNid node) ns
|
|
-- after verifying a node, check whether it can be a closer neighbour
|
|
else do
|
|
if node `isPossiblePredecessor` ns
|
|
then atomically $ do
|
|
ns' <- readTVar nsSTM
|
|
writeTVar nsSTM $ addPredecessors [node] ns'
|
|
else pure ()
|
|
if node `isPossibleSuccessor` ns
|
|
then atomically $ do
|
|
ns' <- readTVar nsSTM
|
|
writeTVar nsSTM $ addSuccessors [node] ns'
|
|
else pure ()
|
|
) pong
|
|
else pure ()
|
|
)
|
|
|
|
-- check the cache invariant per slice and, if necessary, do a single lookup to the
|
|
-- middle of each slice not verifying the invariant
|
|
latestNs <- readTVarIO nsSTM
|
|
latestCache <- readTVarIO $ nodeCacheSTM latestNs
|
|
let nodesToQuery targetID = case queryLocalCache latestNs latestCache (lNumBestNodes latestNs) targetID of
|
|
FOUND node -> [node]
|
|
FORWARD nodeSet -> remoteNode <$> Set.elems nodeSet
|
|
forM_ (checkCacheSliceInvariants latestNs latestCache) (\targetID ->
|
|
forkIO $ sendQueryIdMessages targetID latestNs (Just (1 + jEntriesPerSlice latestNs)) (nodesToQuery targetID) >> pure () -- ask for 1 entry more than j because of querying the middle
|
|
)
|
|
|
|
putStrLn "cache verify run: end"
|
|
threadDelay $ 10^6 * round maxEntryAge `div` 20
|
|
|
|
|
|
-- | Checks the invariant of at least @jEntries@ per cache slice.
|
|
-- If this invariant does not hold, the middle of the slice is returned for
|
|
-- making lookups to that ID
|
|
checkCacheSliceInvariants :: LocalNodeState
|
|
-> NodeCache
|
|
-> [NodeID] -- ^ list of middle IDs of slices not
|
|
-- ^ fulfilling the invariant
|
|
checkCacheSliceInvariants ns
|
|
| isJoined ns = checkPredecessorSlice jEntries (getNid ns) startBound lastPred <> checkSuccessorSlice jEntries (getNid ns) startBound lastSucc
|
|
| otherwise = const []
|
|
where
|
|
jEntries = jEntriesPerSlice ns
|
|
lastPred = getNid <$> lastMay (predecessors ns)
|
|
lastSucc = getNid <$> lastMay (successors ns)
|
|
-- start slice boundary: 1/2 key space
|
|
startBound = getNid ns + 2^(idBits - 1)
|
|
|
|
checkSuccessorSlice :: Integral i => i -> NodeID -> NodeID -> Maybe NodeID -> NodeCache -> [NodeID]
|
|
checkSuccessorSlice _ _ _ Nothing _ = []
|
|
checkSuccessorSlice j ownID upperBound (Just lastSuccID) cache
|
|
| (upperBound `localCompare` lastSuccID) == LT = []
|
|
| otherwise =
|
|
let
|
|
diff = getNodeID $ upperBound - ownID
|
|
lowerBound = ownID + fromInteger (diff `div` 2)
|
|
middleID = lowerBound + fromInteger (diff `div` 4)
|
|
lookupResult = Set.map (getNid . remoteNode) $ closestCachePredecessors jEntries upperBound cache
|
|
in
|
|
-- check whether j entries are in the slice
|
|
if length lookupResult == jEntries
|
|
&& all (\r -> (r `localCompare` lowerBound) == GT) lookupResult
|
|
&& all (\r -> (r `localCompare` upperBound) == LT) lookupResult
|
|
then checkSuccessorSlice j ownID (lowerBound - 1) (Just lastSuccID) cache
|
|
-- if not enough entries, add the middle of the slice to list
|
|
else middleID : checkSuccessorSlice j ownID (lowerBound - 1) (Just lastSuccID) cache
|
|
|
|
checkPredecessorSlice :: Integral i => i -> NodeID -> NodeID -> Maybe NodeID -> NodeCache -> [NodeID]
|
|
checkPredecessorSlice _ _ _ Nothing _ = []
|
|
checkPredecessorSlice j ownID lowerBound (Just lastPredID) cache
|
|
| (lowerBound `localCompare` lastPredID) == GT = []
|
|
| otherwise =
|
|
let
|
|
diff = getNodeID $ ownID - lowerBound
|
|
upperBound = ownID - fromInteger (diff `div` 2)
|
|
middleID = lowerBound + fromInteger (diff `div` 4)
|
|
lookupResult = Set.map (getNid . remoteNode) $ closestCachePredecessors jEntries upperBound cache
|
|
in
|
|
-- check whether j entries are in the slice
|
|
if length lookupResult == jEntries
|
|
&& all (\r -> (r `localCompare` lowerBound) == GT) lookupResult
|
|
&& all (\r -> (r `localCompare` upperBound) == LT) lookupResult
|
|
then checkPredecessorSlice j ownID (upperBound + 1) (Just lastPredID) cache
|
|
-- if not enough entries, add the middle of the slice to list
|
|
else middleID : checkPredecessorSlice j ownID (upperBound + 1) (Just lastPredID) cache
|
|
|
|
|
|
-- | Periodically send @StabiliseRequest' s to the closest neighbour nodes, until
|
|
-- one responds, and get their neighbours for maintaining the own neighbour lists.
|
|
-- If necessary, request new neighbours.
|
|
stabiliseThread :: LocalNodeStateSTM -> IO ()
|
|
stabiliseThread nsSTM = forever $ do
|
|
ns <- readTVarIO nsSTM
|
|
|
|
putStrLn "stabilise run: begin"
|
|
print ns
|
|
|
|
-- iterate through the same snapshot, collect potential new neighbours
|
|
-- and nodes to be deleted, and modify these changes only at the end of
|
|
-- each stabilise run.
|
|
-- This decision makes iterating through a potentially changing list easier.
|
|
|
|
-- don't contact all neighbours unless the previous one failed/ Left ed
|
|
|
|
predStabilise <- stabiliseClosestResponder ns predecessors 1 []
|
|
succStabilise <- stabiliseClosestResponder ns predecessors 1 []
|
|
|
|
let
|
|
(predDeletes, predNeighbours) = either (const ([], [])) id predStabilise
|
|
(succDeletes, succNeighbours) = either (const ([], [])) id succStabilise
|
|
allDeletes = predDeletes <> succDeletes
|
|
allNeighbours = predNeighbours <> succNeighbours
|
|
|
|
-- now actually modify the node state's neighbours
|
|
updatedNs <- atomically $ do
|
|
newerNsSnap <- readTVar nsSTM
|
|
let
|
|
-- sorting and taking only k neighbours is taken care of by the
|
|
-- setSuccessors/ setPredecessors functions
|
|
newPreds = (predecessors newerNsSnap \\ allDeletes) <> allNeighbours
|
|
newSuccs = (successors newerNsSnap \\ allDeletes) <> allNeighbours
|
|
newNs = setPredecessors newPreds . setSuccessors newSuccs $ newerNsSnap
|
|
writeTVar nsSTM newNs
|
|
pure newNs
|
|
-- delete unresponding nodes from cache as well
|
|
mapM_ (atomically . writeTQueue (cacheWriteQueue updatedNs) . deleteCacheEntry . getNid) allDeletes
|
|
|
|
-- try looking up additional neighbours if list too short
|
|
forM_ [(length $ predecessors updatedNs)..(kNeighbours updatedNs)] (\_ -> do
|
|
ns' <- readTVarIO nsSTM
|
|
nextEntry <- requestQueryID ns' $ pred . getNid $ lastDef (toRemoteNodeState ns') (predecessors ns')
|
|
atomically $ do
|
|
latestNs <- readTVar nsSTM
|
|
writeTVar nsSTM $ addPredecessors [nextEntry] latestNs
|
|
)
|
|
|
|
forM_ [(length $ successors updatedNs)..(kNeighbours updatedNs)] (\_ -> do
|
|
ns' <- readTVarIO nsSTM
|
|
nextEntry <- requestQueryID ns' $ succ . getNid $ lastDef (toRemoteNodeState ns') (successors ns')
|
|
atomically $ do
|
|
latestNs <- readTVar nsSTM
|
|
writeTVar nsSTM $ addSuccessors [nextEntry] latestNs
|
|
)
|
|
|
|
putStrLn "stabilise run: end"
|
|
-- TODO: make delay configurable
|
|
threadDelay (60 * 10^6)
|
|
where
|
|
-- | send a stabilise request to the n-th neighbour
|
|
-- (specified by the provided getter function) and on failure retr
|
|
-- with the n+1-th neighbour.
|
|
-- On success, return 2 lists: The failed nodes and the potential neighbours
|
|
-- returned by the queried node.
|
|
stabiliseClosestResponder :: LocalNodeState -- ^ own node
|
|
-> (LocalNodeState -> [RemoteNodeState]) -- ^ getter function for either predecessors or successors
|
|
-> Int -- ^ index of neighbour to query
|
|
-> [RemoteNodeState] -- ^ delete accumulator
|
|
-> IO (Either String ([RemoteNodeState], [RemoteNodeState])) -- ^ (nodes to be deleted, successfully pinged potential neighbours)
|
|
stabiliseClosestResponder ns neighbourGetter neighbourNum deleteAcc
|
|
| isNothing (currentNeighbour ns neighbourGetter neighbourNum) = pure $ Left "exhausted all neigbours"
|
|
| otherwise = do
|
|
let node = fromJust $ currentNeighbour ns neighbourGetter neighbourNum
|
|
stabResponse <- requestStabilise ns node
|
|
case stabResponse of
|
|
-- returning @Left@ signifies the need to try again with next from list
|
|
Left err -> stabiliseClosestResponder ns neighbourGetter (neighbourNum+1) (node:deleteAcc)
|
|
Right (succs, preds) -> do
|
|
-- ping each returned node before actually inserting them
|
|
-- send pings in parallel, check whether ID is part of the returned IDs
|
|
pingThreads <- mapM (async . checkReachability ns) $ preds <> succs
|
|
-- ToDo: exception handling, maybe log them
|
|
-- filter out own node
|
|
checkedNeighbours <- filter (/= toRemoteNodeState ns) . catMaybes . rights <$> mapM waitCatch pingThreads
|
|
pure $ Right (deleteAcc, checkedNeighbours)
|
|
|
|
|
|
currentNeighbour ns neighbourGetter = atMay $ neighbourGetter ns
|
|
|
|
checkReachability :: LocalNodeState -- ^ this node
|
|
-> RemoteNodeState -- ^ node to Ping for reachability
|
|
-> IO (Maybe RemoteNodeState) -- ^ if the Pinged node handles the requested node state then that one
|
|
checkReachability ns toCheck = do
|
|
resp <- requestPing ns toCheck
|
|
pure $ either (const Nothing) (\vss ->
|
|
if toCheck `elem` vss then Just toCheck else Nothing
|
|
) resp
|
|
|
|
|
|
-- | Receives UDP packets and passes them to other threads via the given TQueue.
|
|
-- Shall be used as the single receiving thread on the server socket, as multiple
|
|
-- threads blocking on the same socket degrades performance.
|
|
recvThread :: Socket -- ^ server socket to receive packets from
|
|
-> TQueue (BS.ByteString, SockAddr) -- ^ receive queue
|
|
-> IO ()
|
|
recvThread sock recvQ = forever $ do
|
|
packet <- recvFrom sock 65535
|
|
atomically $ writeTQueue recvQ packet
|
|
|
|
-- | Only thread to send data it gets from a TQueue through the server socket.
|
|
sendThread :: Socket -- ^ server socket used for sending
|
|
-> TQueue (BS.ByteString, SockAddr) -- ^ send queue
|
|
-> IO ()
|
|
sendThread sock sendQ = forever $ do
|
|
(packet, addr) <- atomically $ readTQueue sendQ
|
|
sendAllTo sock packet addr
|
|
|
|
-- | Sets up and manages the main server threads of FediChord
|
|
fediMainThreads :: Socket -> LocalNodeStateSTM -> IO ()
|
|
fediMainThreads sock nsSTM = do
|
|
(\x -> putStrLn $ "launching threads, ns: " <> show x) =<< readTVarIO nsSTM
|
|
sendQ <- newTQueueIO
|
|
recvQ <- newTQueueIO
|
|
-- concurrently launch all handler threads, if one of them throws an exception
|
|
-- all get cancelled
|
|
concurrently_
|
|
(fediMessageHandler sendQ recvQ nsSTM) $
|
|
concurrently_ (stabiliseThread nsSTM) $
|
|
concurrently_ (cacheVerifyThread nsSTM) $
|
|
concurrently_ (convergenceSampleThread nsSTM) $
|
|
concurrently_
|
|
(sendThread sock sendQ)
|
|
(recvThread sock recvQ)
|
|
|
|
|
|
-- defining this here as, for now, the RequestMap is only used by fediMessageHandler.
|
|
-- Once that changes, move to FediChordTypes
|
|
type RequestMap = Map.Map (SockAddr, Integer) RequestMapEntry
|
|
|
|
data RequestMapEntry = RequestMapEntry (Set.Set FediChordMessage) (Maybe Integer)
|
|
POSIXTime
|
|
|
|
-- TODO: make purge age configurable
|
|
-- | periodically clean up old request parts
|
|
responsePurgeAge :: POSIXTime
|
|
responsePurgeAge = 60 -- seconds
|
|
|
|
requestMapPurge :: MVar RequestMap -> IO ()
|
|
requestMapPurge mapVar = forever $ do
|
|
rMapState <- takeMVar mapVar
|
|
now <- getPOSIXTime
|
|
putMVar mapVar $ Map.filter (\entry@(RequestMapEntry _ _ ts) ->
|
|
now - ts < responsePurgeAge
|
|
) rMapState
|
|
threadDelay $ round responsePurgeAge * 2 * 10^6
|
|
|
|
|
|
-- | Wait for messages, deserialise them, manage parts and acknowledgement status,
|
|
-- and pass them to their specific handling function.
|
|
fediMessageHandler :: TQueue (BS.ByteString, SockAddr) -- ^ send queue
|
|
-> TQueue (BS.ByteString, SockAddr) -- ^ receive queue
|
|
-> LocalNodeStateSTM -- ^ acting NodeState
|
|
-> IO ()
|
|
fediMessageHandler sendQ recvQ nsSTM = do
|
|
-- Read node state just once, assuming that all relevant data for this function does
|
|
-- not change.
|
|
-- Other functions are passed the nsSTM reference and thus can get the latest state.
|
|
nsSnap <- readTVarIO nsSTM
|
|
-- handling multipart messages:
|
|
-- Request parts can be insert into a map (key: (sender IP against spoofing, request ID), value: timestamp + set of message parts, handle all of them when size of set == parts) before being handled. This map needs to be purged periodically by a separate thread and can be protected by an MVar for fairness.
|
|
requestMap <- newMVar (Map.empty :: RequestMap)
|
|
-- run receive loop and requestMapPurge concurrently, so that an exception makes
|
|
-- both of them fail
|
|
concurrently_ (requestMapPurge requestMap) $ forever $ do
|
|
-- wait for incoming messages
|
|
(rawMsg, sourceAddr) <- atomically $ readTQueue recvQ
|
|
let aMsg = deserialiseMessage rawMsg
|
|
either (\_ ->
|
|
-- drop invalid messages
|
|
pure ()
|
|
)
|
|
(\validMsg ->
|
|
case validMsg of
|
|
aRequest@Request{}
|
|
-- if not a multipart message, handle immediately. Response is at the same time an ACK
|
|
| part aRequest == 1 && isFinalPart aRequest ->
|
|
forkIO (handleIncomingRequest nsSTM sendQ (Set.singleton aRequest) sourceAddr) >> pure ()
|
|
-- otherwise collect all message parts first before handling the whole request
|
|
| otherwise -> do
|
|
now <- getPOSIXTime
|
|
-- critical locking section of requestMap
|
|
rMapState <- takeMVar requestMap
|
|
-- insert new message and get set
|
|
let
|
|
theseMaxParts = if isFinalPart aRequest then Just (part aRequest) else Nothing
|
|
thisKey = (sourceAddr, requestID aRequest)
|
|
newMapState = Map.insertWith (\
|
|
(RequestMapEntry thisMsgSet p' ts) (RequestMapEntry oldMsgSet p'' _) ->
|
|
RequestMapEntry (thisMsgSet `Set.union` oldMsgSet) (p' <|> p'') ts
|
|
)
|
|
thisKey
|
|
(RequestMapEntry (Set.singleton aRequest) theseMaxParts now)
|
|
rMapState
|
|
-- put map back into MVar, end of critical section
|
|
putMVar requestMap newMapState
|
|
-- ACK the received part
|
|
forM_ (ackRequest (getNid nsSnap) aRequest) $
|
|
\msg -> atomically $ writeTQueue sendQ (msg, sourceAddr)
|
|
-- if all parts received, then handle request.
|
|
let
|
|
(RequestMapEntry theseParts mayMaxParts _) = fromJust $ Map.lookup thisKey newMapState
|
|
numParts = Set.size theseParts
|
|
if maybe False (numParts ==) (fromIntegral <$> mayMaxParts)
|
|
then forkIO (handleIncomingRequest nsSTM sendQ theseParts sourceAddr) >> pure()
|
|
else pure()
|
|
-- Responses should never arrive on the main server port, as they are always
|
|
-- responses to requests sent from dedicated sockets on another port
|
|
_ -> pure ()
|
|
)
|
|
aMsg
|
|
|
|
pure ()
|