-- | Erlang style supervisors for Haskell. -- Note that yet, these are not really good enough for using in other projects. -- are currently subject to change until I figure out how a nice interface will -- look like. At that moment they could be split off into their own package. {-# LANGUAGE ScopedTypeVariables #-} module Supervisor ( -- * Types Child(..) , Children , SupervisorMsg(..) , SupervisorChan -- * Supervisor Initialization , allForOne , oneForOne -- * helper calls , pDie , defaultStopHandler ) where import Control.Applicative import Control.Concurrent import Control.Concurrent.CML import Control.Monad.State import Control.Monad.Reader import Prelude hiding (catch) import Process data Child = Supervisor (SupervisorChan -> IO ThreadId) | Worker (SupervisorChan -> IO ThreadId) data SupervisorMsg = IAmDying ThreadId | PleaseDie ThreadId | SpawnNew Child type SupervisorChan = Channel SupervisorMsg type Children = [Child] data ChildInfo = HSupervisor ThreadId | HWorker ThreadId pDie :: SupervisorChan -> IO () pDie supC = do tid <- myThreadId sync $ transmit supC $ IAmDying tid class SupervisorConf a where getParent :: a -> SupervisorChan getChan :: a -> SupervisorChan data CFOFA = CFOFA { name :: String , chan :: SupervisorChan , parent :: SupervisorChan } instance SupervisorConf CFOFA where getParent = parent getChan = chan instance Logging CFOFA where logName = name data STOFA = STOFA { childInfo :: [ChildInfo] } -- | Run a set of processes and do it once in the sense that if someone dies, -- no restart is attempted. We will just kill off everybody without any kind -- of prejudice. allForOne :: String -> Children -> SupervisorChan -> IO ThreadId allForOne name children parentC = do c <- channel spawnP (CFOFA name c parentC) (STOFA []) (catchP startup (defaultStopHandler parentC)) where startup = do childs <- mapM spawnChild children modify (\_ -> STOFA (reverse childs)) forever eventLoop eventLoop = do mTid <- liftIO myThreadId syncP =<< chooseP [childEvent, parentEvent mTid] childEvent = do ev <- recvPC chan wrapP ev (\msg -> case msg of IAmDying tid -> do gets childInfo >>= mapM_ finChild t <- liftIO myThreadId sendPC parent (IAmDying t) >>= syncP SpawnNew chld -> do n <- spawnChild chld modify (\(STOFA cs) -> STOFA (n : cs))) parentEvent mTid = do ev <- recvP parentC (\m -> case m of PleaseDie tid | tid == mTid -> True _ -> False) wrapP ev (\msg -> case msg of PleaseDie _ -> gets childInfo >>= mapM_ finChild _ -> return ()) data CFOFO = CFOFO { oName :: String , oChan :: SupervisorChan , oparent :: SupervisorChan } instance SupervisorConf CFOFO where getParent = oparent getChan = oChan instance Logging CFOFO where logName = oName data STOFO = STOFO [ChildInfo] -- | A One-for-one supervisor is called with @oneForOne children parentCh@. It will spawn and run -- @children@ and be linked into the supervisor structure on @parentCh@. It returns the ThreadId -- of the supervisor itself and the Channel of which it is the controller. -- -- Should a process die, the one-for-one supervisor will do nothing about it. It will just record -- the death and let the other processes keep running. -- -- TODO: Restart policies. oneForOne :: String -> Children -> SupervisorChan -> IO (ThreadId, SupervisorChan) oneForOne name children parentC = do c <- channel t <- spawnP (CFOFO name c parentC) (STOFO []) (catchP startup (defaultStopHandler parentC)) return (t, c) where startup :: Process CFOFO STOFO () startup = do childs <- mapM spawnChild children modify (\_ -> STOFO (reverse childs)) forever eventLoop eventLoop :: Process CFOFO STOFO () eventLoop = do mTid <- liftIO myThreadId syncP =<< chooseP [childEvent, parentEvent mTid] childEvent = do ev <- recvPC oChan wrapP ev (\msg -> case msg of IAmDying tid -> pruneChild tid SpawnNew chld -> do n <- spawnChild chld modify (\(STOFO cs) -> STOFO (n : cs))) parentEvent mTid = do ev <- recvP parentC (\m -> case m of PleaseDie tid | tid == mTid -> True _ -> False) wrapP ev (\msg -> do (STOFO cs) <- get mapM_ finChild cs stopP) pruneChild tid = modify (\(STOFO cs) -> STOFO (filter check cs)) where check (HSupervisor t) = t == tid check (HWorker t) = t == tid finChild :: SupervisorConf a => ChildInfo -> Process a b () finChild (HWorker tid) = liftIO $ killThread tid -- Make this call killP in Process? finChild (HSupervisor tid) = do c <- getChan <$> ask syncP =<< sendP c (PleaseDie tid) spawnChild :: SupervisorConf a => Child -> Process a b ChildInfo spawnChild (Worker proc) = do c <- getChan <$> ask tid <- liftIO $ proc c return $ HWorker tid spawnChild (Supervisor proc) = do c <- getChan <$> ask tid <- liftIO $ proc c return $ HSupervisor tid defaultStopHandler :: SupervisorChan -> Process a b () defaultStopHandler supC = do t <- liftIO $ myThreadId syncP =<< (sendP supC $ IAmDying t)