git.iko.soy/distributed-process
on master help sign in
Properly implement peer discovery
commit81ce9d5d3ed03d6264984fb3ebcae53fc30e7d57 authorEdsko de Vries <edsko@edsko.net> dateThu 5 Jul 2012, 10:47 (14 years ago) parent9cc20bb
5 files changed+241−41
distributed-process/distributed-process.cabal+23−3 distributed-process/src/Control/Distributed/Process/Backend/Local.hs+80−36 distributed-process/src/Control/Distributed/Process/Internal/Multicast.hs+124 distributed-process/tests/TestLocalBackend.hs+6−2 distributed-process/tests/runTestLocalBackend.hs+8
distributed-process/distributed-process.cabal+23−3
@@ -29,7 +29,7 @@ Library
2929 Control.Distributed.Process.Closure,
3030 Control.Distributed.Process.Node
3131 Control.Distributed.Process.Backend.Local
32− Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs
32+ Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs, ImpredicativeTypes
3333 ghc-options: -Wall
3434 HS-Source-Dirs: src
3535
@@ -49,7 +49,7 @@ Test-Suite TestCH
4949 stm,
5050 ghc-prim,
5151 network-transport >= 0.1
52− Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs
52+ Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs, ImpredicativeTypes
5353 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind
5454 HS-Source-Dirs: tests src
5555
@@ -70,6 +70,26 @@ Test-Suite TestClosure
7070 template-haskell,
7171 stm,
7272 network-transport >= 0.1
73− Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs
73+ Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs, ImpredicativeTypes
74+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind
75+ HS-Source-Dirs: tests src
76+
77+-- Not a proper test, but we want to use cabal to compile it
78+Test-Suite TestLocalBackend
79+ Type: exitcode-stdio-1.0
80+ Main-Is: TestLocalBackend.hs
81+ Build-Depends: base,
82+ bytestring,
83+ containers,
84+ mtl,
85+ binary,
86+ transformers,
87+ data-accessor,
88+ random,
89+ ansi-terminal,
90+ template-haskell,
91+ stm,
92+ network-transport >= 0.1
93+ Extensions: ExplicitForAll, ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, UndecidableInstances, MultiParamTypeClasses, DeriveDataTypeable, TemplateHaskell, ExistentialQuantification, RankNTypes, GADTs, ImpredicativeTypes
7494 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fno-warn-unused-do-bind
7595 HS-Source-Dirs: tests src
distributed-process/src/Control/Distributed/Process/Backend/Local.hs+80−36
@@ -1,18 +1,20 @@
11-- | Simple backend based on the TCP transport which offers node discovery
22-- based on UDP multicast
3+{-# OPTIONS_GHC -fno-warn-orphans #-}
34module Control.Distributed.Process.Backend.Local
45 ( LocalBackend(..)
56 , initializeBackend
67 ) where
78
8−import Data.Binary (encode)
9−import qualified Data.ByteString as BSS (concat, append)
10−import qualified Data.ByteString.Lazy as BSL (toChunks, length)
9+import Data.Binary (Binary(get, put), getWord8, putWord8)
10+import Data.Accessor (Accessor, accessor, (^:), (^.))
11+import Data.Set (Set)
12+import qualified Data.Set as Set (insert, empty, toList)
1113import Control.Applicative ((<$>))
1214import Control.Exception (throw)
13−import Control.Monad (forM_)
14−import Control.Concurrent (forkIO)
15−import Control.Concurrent.MVar (MVar, newMVar, readMVar)
15+import Control.Monad (forM_, forever)
16+import Control.Concurrent (forkIO, threadDelay)
17+import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_)
1618import Control.Distributed.Process.Internal.Types
1719 ( LocalNode
1820 , RemoteTable
@@ -26,55 +28,97 @@ import qualified Network.Transport.TCP as NT
2628 ( createTransport
2729 , defaultTCPParameters
2830 )
29−import Network.Transport.Internal (encodeInt32, decodeInt32)
30−import qualified Network.Transport as NT ()
31−import qualified Network.Socket as N (HostName, ServiceName, Socket, SockAddr)
32−import qualified Network.Socket.ByteString as NBS (recvFrom, sendTo)
33−import qualified Network.Multicast as NM (multicastSender, multicastReceiver)
31+import qualified Network.Transport as NT (Transport)
32+import qualified Network.Socket as N (HostName, ServiceName, SockAddr)
33+import Control.Distributed.Process.Internal.Multicast (initMulticast)
3434
35+-- | Local backend
3536data LocalBackend = LocalBackend {
37+ -- | Create a new local node
3638 newLocalNode :: IO LocalNode
37− , findPeers :: IO [NodeId]
39+ -- | @findPeers t@ sends out a /who's there?/ request, waits 't' msec,
40+ -- and then collects and returns the answers
41+ , findPeers :: Int -> IO [NodeId]
3842 }
3943
4044data LocalBackendState = LocalBackendState {
41− localNodes :: [LocalNode]
45+ _localNodes :: [LocalNode]
46+ , _peers :: Set NodeId
4247 }
4348
4449-- | Initialize the backend
4550initializeBackend :: N.HostName -> N.ServiceName -> RemoteTable -> IO LocalBackend
4651initializeBackend host port rtable = do
47− mTransport <- NT.createTransport host port NT.defaultTCPParameters
48− mcastSender <- NM.multicastSender "224.0.0.99" 9999
49− mcastReceiver <- NM.multicastReceiver "224.0.0.99" 9999
50− backendState <- newMVar LocalBackendState { localNodes = [] }
51− forkIO $ peerDiscoveryDaemon backendState mcastReceiver mcastSender
52+ mTransport <- NT.createTransport host port NT.defaultTCPParameters
53+ (recv, send) <- initMulticast "224.0.0.99" 9999 1024
54+ backendState <- newMVar LocalBackendState
55+ { _localNodes = []
56+ , _peers = Set.empty
57+ }
58+ forkIO $ peerDiscoveryDaemon backendState recv send
5259 case mTransport of
5360 Left err -> throw err
5461 Right transport -> return LocalBackend {
55− newLocalNode = Node.newLocalNode transport rtable
56− , findPeers = apiFindPeers mcastSender
62+ newLocalNode = apiNewLocalNode transport rtable backendState
63+ , findPeers = apiFindPeers send backendState
5764 }
5865
66+-- | Create a new local node
67+apiNewLocalNode :: NT.Transport
68+ -> RemoteTable
69+ -> MVar LocalBackendState
70+ -> IO LocalNode
71+apiNewLocalNode transport rtable backendState = do
72+ localNode <- Node.newLocalNode transport rtable
73+ modifyMVar_ backendState $ return . (localNodes ^: (localNode :))
74+ return localNode
75+
5976-- | Peer discovery
60−apiFindPeers :: (N.Socket, N.SockAddr) -> IO [NodeId]
61−apiFindPeers (sock, addr) = do
62− NBS.sendTo sock (encodeInt32 (0xCDAECDAE :: Int)) addr
63− return []
77+apiFindPeers :: (PeerDiscoveryMsg -> IO ())
78+ -> MVar LocalBackendState
79+ -> Int
80+ -> IO [NodeId]
81+apiFindPeers send backendState delay = do
82+ send PeerDiscoveryRequest
83+ threadDelay delay
84+ Set.toList . (^. peers) <$> readMVar backendState
85+
86+data PeerDiscoveryMsg =
87+ PeerDiscoveryRequest
88+ | PeerDiscoveryReply NodeId
89+
90+instance Binary PeerDiscoveryMsg where
91+ put PeerDiscoveryRequest = putWord8 0
92+ put (PeerDiscoveryReply nid) = putWord8 1 >> put nid
93+ get = do
94+ header <- getWord8
95+ case header of
96+ 0 -> return PeerDiscoveryRequest
97+ 1 -> PeerDiscoveryReply <$> get
98+ _ -> fail "PeerDiscoveryMsg.get: invalid"
6499
65100-- | Respond to peer discovery requests sent by other nodes
66101peerDiscoveryDaemon :: MVar LocalBackendState
67− -> N.Socket
68− -> (N.Socket, N.SockAddr)
102+ -> IO (PeerDiscoveryMsg, N.SockAddr)
103+ -> (PeerDiscoveryMsg -> IO ())
69104 -> IO ()
70−peerDiscoveryDaemon backendState readSock (writeSock, addr) = do
71− (msgLen, _remoteAddr) <- NBS.recvFrom readSock 4
105+peerDiscoveryDaemon backendState recv send = forever go
106+ where
107+ go = do
108+ (msg, _) <- recv
109+ case msg of
110+ PeerDiscoveryRequest -> do
111+ nodes <- (^. localNodes) <$> readMVar backendState
112+ forM_ nodes $ send . PeerDiscoveryReply . Node.localNodeId
113+ PeerDiscoveryReply nid ->
114+ modifyMVar_ backendState $ return . (peers ^: Set.insert nid)
115+
116+--------------------------------------------------------------------------------
117+-- Accessors --
118+--------------------------------------------------------------------------------
119+
120+localNodes :: Accessor LocalBackendState [LocalNode]
121+localNodes = accessor _localNodes (\ns st -> st { _localNodes = ns })
72122
73− case decodeInt32 msgLen of
74− (0xCDAECDAE :: Int) -> do
75− nodes <- localNodes <$> readMVar backendState
76− forM_ nodes $ \node -> do
77− let nodeAddr = encode (Node.localNodeId node)
78− msg = encodeInt32 (BSL.length nodeAddr) `BSS.append`
79− (BSS.concat . BSL.toChunks $ nodeAddr)
80− NBS.sendTo writeSock msg addr
123+peers :: Accessor LocalBackendState (Set NodeId)
124+peers = accessor _peers (\ps st -> st { _peers = ps })
distributed-process/src/Control/Distributed/Process/Internal/Multicast.hsnew file+124
@@ -0,0 +1,124 @@
1+-- | Multicast utilities
2+{-# OPTIONS_GHC -fno-warn-orphans #-}
3+module Control.Distributed.Process.Internal.Multicast (initMulticast) where
4+
5+import Data.Function (on)
6+import Data.Map (Map)
7+import qualified Data.Map as Map (empty)
8+import Data.Binary (Binary, decode, encode)
9+import Data.IORef (IORef, newIORef, readIORef, modifyIORef)
10+import qualified Data.ByteString as BSS (ByteString, concat)
11+import qualified Data.ByteString.Lazy as BSL
12+ ( ByteString
13+ , empty
14+ , append
15+ , fromChunks
16+ , toChunks
17+ , length
18+ , splitAt
19+ )
20+import Data.Accessor (Accessor, (^:), (^.), (^=))
21+import qualified Data.Accessor.Container as DAC (mapDefault)
22+import Control.Applicative ((<$>))
23+import Network.Socket (HostName, PortNumber, Socket, SockAddr)
24+import qualified Network.Socket.ByteString as NBS (recvFrom, sendManyTo)
25+import Network.Transport.Internal (decodeInt32, encodeInt32)
26+import Network.Multicast (multicastSender, multicastReceiver)
27+
28+--------------------------------------------------------------------------------
29+-- Top-level API --
30+--------------------------------------------------------------------------------
31+
32+-- | Given a hostname and a port number, initialize the multicast system.
33+--
34+-- Note: it is important that you never send messages larger than the maximum
35+-- message size; if you do, all subsequent communication will probably fail.
36+--
37+-- Returns a reader and a writer.
38+initMulticast :: HostName -- ^ Multicast IP
39+ -> PortNumber -- ^ Port number
40+ -> Int -- ^ Maximum message size
41+ -> IO ( forall a. Binary a => IO (a, SockAddr)
42+ , forall a. Binary a => a -> IO ()
43+ )
44+initMulticast host port bufferSize = do
45+ (sendSock, sendAddr) <- multicastSender host port
46+ readSock <- multicastReceiver host port
47+ st <- newIORef Map.empty
48+ return (recvBinary readSock st bufferSize, writer sendSock sendAddr)
49+ where
50+ writer :: forall a. Binary a => Socket -> SockAddr -> a -> IO ()
51+ writer sock addr val = do
52+ let bytes = encode val
53+ len = encodeInt32 (BSL.length bytes)
54+ NBS.sendManyTo sock (len : BSL.toChunks bytes) addr
55+
56+--------------------------------------------------------------------------------
57+-- UDP multicast read, dealing with multiple senders --
58+--------------------------------------------------------------------------------
59+
60+type UDPState = Map SockAddr BSL.ByteString
61+
62+-- TODO: This is inefficient and an orphan instance.
63+-- Requested official instance (https://github.com/haskell/network/issues/38)
64+instance Ord SockAddr where
65+ compare = compare `on` show
66+
67+bufferFor :: SockAddr -> Accessor UDPState BSL.ByteString
68+bufferFor = DAC.mapDefault BSL.empty
69+
70+bufferAppend :: SockAddr -> BSS.ByteString -> UDPState -> UDPState
71+bufferAppend addr bytes =
72+ bufferFor addr ^: flip BSL.append (BSL.fromChunks [bytes])
73+
74+recvBinary :: Binary a => Socket -> IORef UDPState -> Int -> IO (a, SockAddr)
75+recvBinary sock st bufferSize = do
76+ (bytes, addr) <- recvWithLength sock st bufferSize
77+ return (decode bytes, addr)
78+
79+recvWithLength :: Socket
80+ -> IORef UDPState
81+ -> Int
82+ -> IO (BSL.ByteString, SockAddr)
83+recvWithLength sock st bufferSize = do
84+ (len, addr) <- recvExact sock 4 st bufferSize
85+ let n = decodeInt32 . BSS.concat . BSL.toChunks $ len
86+ bytes <- recvExactFrom addr sock n st bufferSize
87+ return (bytes, addr)
88+
89+-- Receive all bytes currently in the buffer
90+recvAll :: Socket -> IORef UDPState -> Int -> IO SockAddr
91+recvAll sock st bufferSize = do
92+ (bytes, addr) <- NBS.recvFrom sock bufferSize
93+ modifyIORef st $ bufferAppend addr bytes
94+ return addr
95+
96+recvExact :: Socket
97+ -> Int
98+ -> IORef UDPState
99+ -> Int
100+ -> IO (BSL.ByteString, SockAddr)
101+recvExact sock n st bufferSize = do
102+ addr <- recvAll sock st bufferSize
103+ bytes <- recvExactFrom addr sock n st bufferSize
104+ return (bytes, addr)
105+
106+recvExactFrom :: SockAddr
107+ -> Socket
108+ -> Int
109+ -> IORef UDPState
110+ -> Int
111+ -> IO BSL.ByteString
112+recvExactFrom addr sock n st bufferSize = go
113+ where
114+ go :: IO BSL.ByteString
115+ go = do
116+ accAddr <- (^. bufferFor addr) <$> readIORef st
117+ if BSL.length accAddr >= fromIntegral n
118+ then do
119+ let (bytes, accAddr') = BSL.splitAt (fromIntegral n) accAddr
120+ modifyIORef st $ bufferFor addr ^= accAddr'
121+ return bytes
122+ else do
123+ _ <- recvAll sock st bufferSize
124+ go
distributed-process/tests/TestLocalBackend.hs+6−2
@@ -2,14 +2,18 @@ import System.Environment (getArgs, getProgName)
22import Control.Distributed.Process.Node (initRemoteTable)
33import Control.Distributed.Process.Backend.Local
44import Control.Concurrent (threadDelay)
5+import Control.Monad (replicateM)
56
67server :: LocalBackend -> IO ()
78server backend = do
8− nodes <- findPeers backend
9+ node <- newLocalNode backend
10+ nodes <- findPeers backend 1000000
911 print nodes
1012
1113client :: LocalBackend -> IO ()
12−client _ = threadDelay 30000000
14+client backend = do
15+ [node1, node2] <- replicateM 2 $ newLocalNode backend
16+ threadDelay 30000000
1317
1418main :: IO ()
1519main = do
distributed-process/tests/runTestLocalBackend.hsnew file+8
@@ -0,0 +1,8 @@
1+#!/bin/bash
2+TestLocalBackend=dist/build/TestLocalBackend/TestLocalBackend
3+$TestLocalBackend client 127.0.0.1 8080 &
4+$TestLocalBackend client 127.0.0.1 8081 &
5+$TestLocalBackend client 127.0.0.1 8082 &
6+$TestLocalBackend client 127.0.0.1 8083 &
7+sleep 2 # give clients a change to start
8+$TestLocalBackend server 127.0.0.1 8084