Gershom Bazerman
NY Haskell Users Group
{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving,
TupleSections, ScopedTypeVariables, TemplateHaskell #-}
import System.Environment (getArgs)
import Network.BSD(getHostName)
import Control.Distributed.Static (staticClosure)
import Control.Distributed.Process
import Control.Distributed.Process.Serializable
import Control.Distributed.Process.Closure
import Control.Distributed.Process.Node hiding (newLocalNode)
import Control.Distributed.Process.Backend.SimpleLocalnet
import Control.Concurrent hiding (newChan)
import qualified Control.Concurrent as C
import Control.Monad
import Control.Monad.State
import Control.Applicative
import Control.Monad.Trans
import Data.Binary hiding (get)
import Data.Monoid
import Data.Typeable
import Data.IORef
import qualified Data.Set as S
import qualified Data.Map as M
"Once we got the interpreter working correctly and had played with it for a while, writing small actors programs, we were astonished to discover that the program fragments in apply that implemented function application and actor invocation were identical!"
(The key is tail call optimization)
Objects are a poor man's closures. Closures are a poor man's object.
Pay no attention
printProcess :: String -> Process ()
printProcess s = liftIO $ putStrLn s
sumProcess :: (Int,Int) -> Process Int
sumProcess (x,y) = do
liftIO . putStrLn $ "summing these: " ++ show (x,y)
return $ x + y
chanProcess = do
sendP <- expect
sendChan sendP "message one"
liftIO $ threadDelay 10000
sendChan sendP "message two"
liftIO $ threadDelay 10000
sendChan sendP "message three"
remotable ['printProcess,'sumProcess,'chanProcess]
main = do
-- Get some instructions
[serverType, port] <- getArgs
-- Set up the context
hostName <- getHostName
distributedContext <- initializeBackend hostName port (__remoteTable initRemoteTable)
-- The first thing a context lets you do is create a node.
node <- newLocalNode distributedContext
-- The other thing a context lets you do is find what other nodes are out there.
putStrLn "Discovering peers"
peers <- findPeers distributedContext 2000
putStrLn "Peers discovered"
-- About the only thing a node lets you do is run processes on it.
runProcess node (go serverType distributedContext peers)
Our hierarchy is Host/Port (location) --> Nodes -> Processes
go :: String -> Backend -> [NodeId] -> Process ()
go "boring" dc ns = do
liftIO $ putStrLn "IO within a process"
return ()
go "volunteer" dc ns = do
() <- expect
return ()
go "remotePrint" dc ns = do
let tellPrint n = spawn n $ $(mkClosure 'printProcess) "hi there!"
mapM_ tellPrint ns
return ()
{- printProcess s = liftIO $ putStrLn s -}
go "remotePrintBad" dc ns = do
let tellPrint n = spawn n $ $(mkClosure 'printProcess) (123::Int)
-- Oh no! What happened to my static types!!!?
mapM_ tellPrint ns
return ()
{- printProcess s = liftIO $ putStrLn s -}
(Like sending a text message in french to an anglophone).
go "remoteCall" dc ns = do
let doIt n = call $(functionTDict 'sumProcess) n $
$(mkClosure 'sumProcess) (12::Int,25::Int)
res <- mapM doIt ns
liftIO $ putStrLn $ "returned: " ++ show res
{-
sumProcess :: (Int,Int) -> Process Int
sumProcess (x,y) = do
liftIO . putStrLn $ "summing these: " ++ show (x,y)
return $ x + y
-}
Interprocess communication is always strict (the wire enforces this).
go "remoteChan" dc ns = do
(sendP,recP) <- newChan
let doIt n = do
pid <- spawn n $ staticClosure $(mkStatic 'chanProcess)
send pid sendP
mapM_ doIt ns
forever $ liftIO . putStrLn =<< receiveChan recP
{-
chanProcess = do
sendP <- expect
sendChan sendP "message one"
liftIO $ threadDelay 10000
sendChan sendP "message two"
liftIO $ threadDelay 10000
sendChan sendP "message three"
-}
newtype DMVar a = DMVar ProcessId deriving (Binary, Typeable)
newtype MVTake = MVTake ProcessId deriving (Binary, Typeable)
newtype MVPut a = MVPut (a,ProcessId) deriving (Binary, Typeable)
newtype MVResponse a = MVResponse {getMVResponse :: a} deriving (Binary, Typeable)
newtype MVSuccess = MVSuccess () deriving (Binary, Typeable)
newDMVar :: forall a. Serializable a => Process (DMVar a)
newDMVar = do
mv <- liftIO $ (newEmptyMVar :: IO (MVar a))
-- Note that this really is forever. No systemwide GC :-(
p <- spawnLocal $ forever $ receiveWait
[
-- note the extra spawnlocal
match $ \(MVTake pid) -> spawnLocal $
liftIO (takeMVar mv) >>= send pid . MVResponse
, match $ \(MVPut (v,pid)) -> spawnLocal $
liftIO (putMVar mv v) >> send pid (MVSuccess ())
]
return $ DMVar p
takeDMVar :: Serializable a => DMVar a -> Process a
takeDMVar (DMVar pid) = do
spid <- getSelfPid
send pid (MVTake spid)
link pid
res <- getMVResponse <$> expect
unlink pid
return res
putDMVar (DMVar pid) x = do
spid <- getSelfPid
send pid $ MVPut (x,spid)
link pid
MVSuccess () <- expect
unlink pid
return ()
The general pattern: Process wrap state, messages interact with processes.
Note that standard Chans have only the send end serializable
newtype DChan a = DChan (SendPort a, ProcessId) deriving (Binary, Typeable)
newtype DCSubscribe = DCSubscribe ProcessId deriving (Binary, Typeable)
newtype DCDie = DCDie () deriving (Binary, Typeable)
newtype DCResponse a = DCResponse {getDCResponse :: a} deriving (Binary, Typeable)
newDChan :: forall a. Serializable a => Process (DChan a)
newDChan = do
(sendP,recP) <- newChan
localChan <- liftIO $ (C.newChan :: IO (C.Chan a))
p <- spawnLocal $ getSelfPid >>= \mp -> forever . receiveWait $
[
match $ \(DCSubscribe pid) -> do
newChan <- liftIO $ dupChan localChan
spawnLocal $ do
link pid
link mp
forever $ send pid . DCResponse =<< liftIO (readChan newChan)
, match $ \(DCDie ()) -> terminate
]
return $ DChan (sendP, p)
writeDChan (DChan (sp,_)) x = sendChan sp
subscribeDChanGen (DChan (_,pid)) f = do
spid <- getSelfPid
spawnLocal $ do
link spid
send pid . DCSubscribe =<< getSelfPid
forever $ f . getDCResponse =<< expect
return ()
subscribeDChan dc f = do
localChan <- liftIO $ C.newChan
subscribeDChanGen dc (liftIO . writeChan localChan)
return $ readChan localChan
subscribeCommutativeMonoid dc f = do
localVar <- liftIO $ newIORef mempty
subscribeDChanGen dc (\x -> liftIO $ atomicModifyIORef localVar (\v -> (v `mappend` x,())))
return $ readIORef localVar
--Value types
newtype DBarrier = DBarrier ProcessId deriving (Binary, Typeable)
newtype Enrolled a = Enrolled a deriving (Binary, Typeable)
-- Message types
newtype DBSync = DBSync ProcessId deriving (Binary, Typeable)
newtype DBEnroll = DBEnroll ProcessId deriving (Binary, Typeable)
newtype DBResign = DBResign ProcessId deriving (Typeable, Binary)
newtype DBClear = DBClear () deriving (Typeable, Binary)
By the way we can use strings instead of newtypes
newDBarrier = do
enrolledSet <- liftIO $ newMVar (S.empty :: S.Set ProcessId,
S.empty :: S.Set ProcessId,
M.empty :: M.Map ProcessId MonitorRef)
let resign pid = do
mr <- liftIO . modifyMVar enrolledSet $ \(enrolled,synced,mrmap) ->
return ((S.delete pid enrolled,
S.delete pid synced,
M.delete pid mrmap),
M.lookup pid mrmap)
maybe (return ()) unmonitor mr
p <- spawnLocal $ forever . receiveWait $
[
match $ \(DBEnroll pid) -> do
mr <- monitor pid
liftIO . modifyMVar_ enrolledSet $ \(enrolled,synced,mrmap) ->
return (S.insert pid enrolled,
synced,
M.insert pid mr mrmap),
match $ \(DBSync pid) -> do
releaseList <- liftIO . modifyMVar enrolledSet $
\(enrolled,synced,mrmap) ->
let synced' = S.insert pid synced
in return $
if enrolled == synced'
then ((enrolled,S.empty,mrmap),synced')
else ((enrolled,synced',mrmap),S.empty)
mapM_ (`send` DBClear ()) $ S.toList releaseList,
match $ \(DBResign pid) -> resign pid,
match $ \(ProcessMonitorNotification mr pid dr) -> resign pid
]
return $ DBarrier p
dbEnroll db@(DBarrier pid) = do
spid <- getSelfPid
send pid (DBEnroll spid)
return $ Enrolled db
dbSync (Enrolled db@(DBarrier pid)) = do
spid <- getSelfPid
send pid $ DBSync spid
link pid
DBClear () <- expect
unlink pid
return db
-- A Lamport Clock is a simple logical clock
-- It tracks an abstract notion of time, not wall-clock time.
-- Every clock has a time and a location (process).
newtype LamportClock = LamportClock (Integer, ProcessId)
deriving (Eq, Ord, Binary, Typeable)
-- When a clock sees the state of another clock, it sets its time
-- to be greater than the max of the two times.
updateClock :: LamportClock -> LamportClock -> LamportClock
updateClock (LamportClock (xi, xpid)) (LamportClock (yi,_)) =
LamportClock (max xi yi + 1, xpid)
newtype LamProcess a = LamProcess (StateT LamportClock Process a)
deriving (Functor, Applicative, Monad)
getClock = LamProcess get
incrClock = LamProcess . modify $ \(LamportClock (xi,xpid)) ->
LamportClock (xi+1,xpid)
-- Only safe for lifting single message primitives.
liftP :: Process a -> LamProcess a
liftP x = incrClock >> LamProcess (lift x)
-- IO doesn't increment clock
instance MonadIO LamProcess where
liftIO x = LamProcess $ lift . liftIO $ x
lsend :: Serializable a => ProcessId -> a -> LamProcess ()
lsend pid x = getClock >>= \c -> liftP $ send pid (c,x)
lexpectClocked :: forall a. Serializable a => LamProcess (LamportClock,a)
lexpectClocked = liftP expect >>= \(c,x) -> LamProcess $
modify (updateClock c) >> return (c,x)
lexpect :: forall a. Serializable a => LamProcess a
lexpect = snd <$> lexpectClocked
This old gem:
punk!
Now go and write some programs!