packages feed

threads-supervisor 1.0.0.0 → 1.0.1.0

raw patch · 4 files changed

+190/−67 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Control.Concurrent.Supervisor: Child :: !RestartStrategy -> RestartAction -> Child
- Control.Concurrent.Supervisor: data Child
- Control.Concurrent.Supervisor: supervise :: SupervisorSpec -> IO Supervisor
+ Control.Concurrent.Supervisor: instance Exception MonitorRequest
+ Control.Concurrent.Supervisor: instance Show MonitorRequest
+ Control.Concurrent.Supervisor: instance Typeable MonitorRequest
+ Control.Concurrent.Supervisor: monitor :: Supervisor -> Supervisor -> IO ()
+ Control.Concurrent.Supervisor: newSupervisorSpec :: IO SupervisorSpec
+ Control.Concurrent.Supervisor: type SupervisorSpec = Supervisor_ Uninitialised
- Control.Concurrent.Supervisor: newSupervisor :: IO SupervisorSpec
+ Control.Concurrent.Supervisor: newSupervisor :: SupervisorSpec -> IO Supervisor

Files

src/Control/Concurrent/Supervisor.hs view
@@ -9,26 +9,41 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveDataTypeable #-}  module Control.Concurrent.Supervisor -  ( Supervisor+  ( SupervisorSpec+  , Supervisor   , DeadLetter-  , Child(..)   , RestartAction   , SupervisionEvent(..)   , RestartStrategy(..)+  -- * Creating a new supervisor spec+  -- $new+  , newSupervisorSpec+  -- * Creating a new supervisor+  -- $sup   , newSupervisor+  -- * Stopping a supervisor+  -- $shutdown   , shutdownSupervisor+  -- * Accessing Supervisor event log+  -- $log   , eventStream   , activeChildren-  , supervise+  -- * Supervise a forked thread+  -- $fork   , forkSupervised+  -- * Monitor another supervisor+  -- $monitor+  , monitor   ) where  import qualified Data.HashMap.Strict as Map import           Control.Concurrent import           Control.Concurrent.STM import           Data.IORef+import           Data.Typeable import           Control.Exception import           Control.Monad import           Data.Time@@ -42,13 +57,13 @@      NewSupervisor :: {         _ns_myTid    :: !(Maybe ThreadId)       , _ns_children :: !(IORef (Map.HashMap ThreadId Child))-      , _ns_mailbox :: TQueue DeadLetter+      , _ns_mailbox :: TChan DeadLetter       , _ns_eventStream :: TBQueue SupervisionEvent       } -> Supervisor_ Uninitialised      Supervisor :: {         _sp_myTid    :: !(Maybe ThreadId)       , _sp_children :: !(IORef (Map.HashMap ThreadId Child))-      , _sp_mailbox :: TQueue DeadLetter+      , _sp_mailbox :: TChan DeadLetter       , _sp_eventStream :: TBQueue SupervisionEvent       } -> Supervisor_ Initialised @@ -59,7 +74,8 @@ data DeadLetter = DeadLetter ThreadId SomeException  ---------------------------------------------------------------------------------data Child = Child !RestartStrategy RestartAction+data Child = Worker !RestartStrategy RestartAction+           | Supvsr !RestartStrategy !(Supervisor_ Initialised)  -------------------------------------------------------------------------------- type RestartAction = ThreadId -> IO ThreadId@@ -73,40 +89,79 @@    deriving Show  ----------------------------------------------------------------------------------- | Erlang inspired strategies.+-- | Erlang inspired strategies. At the moment only the 'OneForOne' is+-- implemented. data RestartStrategy =      OneForOne      deriving Show +-- $new+-- In order to create a new supervisor, you need a `SupervisorSpec`,+-- which can be acquired by a call to `newSupervisor`:+ ----------------------------------------------------------------------------------- | Creates a new supervisor-newSupervisor :: IO SupervisorSpec-newSupervisor = do-  tkn <- newTQueueIO+-- | Creates a new 'SupervisorSpec'. The reason it doesn't return a+-- 'Supervisor' is to force you to call 'supervise' explicitly, in order to start the+-- supervisor thread.+newSupervisorSpec :: IO SupervisorSpec+newSupervisorSpec = do+  tkn <- newTChanIO   evt <- newTBQueueIO 1000   ref <- newIORef Map.empty   return $ NewSupervisor Nothing ref tkn evt +-- $supervise+ --------------------------------------------------------------------------------+newSupervisor :: SupervisorSpec -> IO Supervisor+newSupervisor spec = forkIO (handleEvents spec) >>= \tid -> do+  mbx <- atomically $ dupTChan (_ns_mailbox spec)+  return $ Supervisor {+    _sp_myTid = Just tid+  , _sp_mailbox = mbx+  , _sp_children = _ns_children spec+  , _sp_eventStream = _ns_eventStream spec+  }++-- $log++--------------------------------------------------------------------------------+-- | Gives you access to the event this supervisor is generating, allowing you+-- to react. It's using a bounded queue to explicitly avoid memory leaks in case+-- you do not want to drain the queue to listen to incoming events. eventStream :: Supervisor -> TBQueue SupervisionEvent eventStream (Supervisor _ _ _ e) = e  --------------------------------------------------------------------------------+-- | Returns the number of active threads at a given moment in time. activeChildren :: Supervisor -> IO Int activeChildren (Supervisor _ chRef _ _) = do   readIORef chRef >>= return . length . Map.keys +-- $shutdown+ -------------------------------------------------------------------------------- -- | Shutdown the given supervisor. This will cause the supervised children to--- be killed as well.+-- be killed as well. To do so, we explore the children tree, killing workers as we go,+-- and recursively calling `shutdownSupervisor` in case we hit a monitored `Supervisor`. shutdownSupervisor :: Supervisor -> IO () shutdownSupervisor (Supervisor sId chRef _ _) = do-  case sId of +  case sId of     Nothing -> return ()     Just tid -> do-      readIORef chRef >>= \chMap -> forM_ (Map.keys chMap) killThread+      chMap <- readIORef chRef+      processChildren (Map.toList chMap)       killThread tid+  where+    processChildren [] = return ()+    processChildren (x:xs) = do+      case x of+        (tid, Worker _ _) -> killThread tid+        (_, Supvsr _ s) -> shutdownSupervisor s+      processChildren xs +-- $fork+ -------------------------------------------------------------------------------- -- | Fork a thread in a supervised mode. forkSupervised :: Supervisor@@ -118,7 +173,7 @@                -> IO ThreadId forkSupervised sup@Supervisor{..} str act =   bracket (supervised sup act) return $ \newChild -> do-    let ch = Child str (const (supervised sup act))+    let ch = Worker str (const (supervised sup act))     atomicModifyIORef' _sp_children $ \chMap -> (Map.insert newChild ch chMap, ())     now <- getCurrentTime     writeIfNotFull _sp_eventStream (ChildBorn newChild now)@@ -134,34 +189,52 @@ supervised :: Supervisor -> IO () -> IO ThreadId supervised Supervisor{..} act = forkFinally act $ \res -> case res of   Left ex -> bracket myThreadId return $ \myId -> atomically $ -    writeTQueue _sp_mailbox (DeadLetter myId ex)+    writeTChan _sp_mailbox (DeadLetter myId ex)   Right _ -> bracket myThreadId return $ \myId -> do     now <- getCurrentTime     atomicModifyIORef' _sp_children $ \chMap -> (Map.delete myId chMap, ())     writeIfNotFull _sp_eventStream (ChildFinished myId now)  ---------------------------------------------------------------------------------supervise :: SupervisorSpec -> IO Supervisor-supervise NewSupervisor{..} = forkIO go >>= \tid -> return $ Supervisor {-  _sp_myTid = Just tid-  , _sp_mailbox = _ns_mailbox-  , _sp_children = _ns_children-  , _sp_eventStream = _ns_eventStream-  }-  where-   go = do-     (DeadLetter newDeath ex) <- atomically $ readTQueue _ns_mailbox-     now <- getCurrentTime-     writeIfNotFull _ns_eventStream (ChildDied newDeath ex now)-     case asyncExceptionFromException ex of-       Just ThreadKilled -> go-       _ -> do-        chMap <- readIORef _ns_children-        case Map.lookup newDeath chMap of-          Nothing -> go-          Just ch@(Child str act) -> case str of-            OneForOne -> do-              newThreadId <- act newDeath-              writeIORef _ns_children (Map.insert newThreadId ch $! Map.delete newDeath chMap)-              writeIfNotFull _ns_eventStream (ChildRestarted newDeath newThreadId str now)-              go+handleEvents :: SupervisorSpec -> IO ()+handleEvents sp@(NewSupervisor myId myChildren myMailbox myStream) = do+  (DeadLetter newDeath ex) <- atomically $ readTChan myMailbox+  now <- getCurrentTime+  writeIfNotFull myStream (ChildDied newDeath ex now)+  case asyncExceptionFromException ex of+    Just ThreadKilled -> handleEvents sp+    _ -> do+     chMap <- readIORef myChildren+     case Map.lookup newDeath chMap of+       Nothing -> handleEvents sp+       Just ch@(Worker str act) -> case str of+         OneForOne -> do+           newThreadId <- act newDeath+           writeIORef myChildren (Map.insert newThreadId ch $! Map.delete newDeath chMap)+           writeIfNotFull myStream (ChildRestarted newDeath newThreadId str now)+           handleEvents sp+       Just ch@(Supvsr str (Supervisor _ mbx cld es)) -> case str of+         OneForOne -> do+           let node = Supervisor myId myChildren myMailbox myStream+           newThreadId <- supervised node (handleEvents $ NewSupervisor Nothing mbx cld es)+           writeIORef myChildren (Map.insert newThreadId ch $! Map.delete newDeath chMap)+           writeIfNotFull myStream (ChildRestarted newDeath newThreadId str now)+           handleEvents sp++-- $monitor++newtype MonitorRequest = MonitoredSupervision ThreadId deriving (Show, Typeable)++instance Exception MonitorRequest++--------------------------------------------------------------------------------+-- | Monitor another supervisor. To achieve these, we simulate a new 'DeadLetter',+-- so that the first supervisor will effectively restart the monitored one.+-- Thanks to the fact that for the supervisor the restart means we just copy over+-- its internal state, it should be perfectly fine to do so.+monitor :: Supervisor -> Supervisor -> IO ()+monitor (Supervisor _ _ mbox _) (Supervisor mbId _ _ _) = do+  case mbId of+    Nothing -> return ()+    Just tid -> atomically $+      writeTChan mbox (DeadLetter tid (toException $ MonitoredSupervision tid))
src/Control/Concurrent/Supervisor/Tutorial.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_GHC -fno-warn-unused-imports #-}  {-| Use @threads-supervisor@ if you want the "poor-man's Erlang supervisors".     @threads-supervisor@ is an IO-based library  with minimal dependencies@@ -85,31 +84,77 @@ -- -- These jobs represent a significant pool of our everyday computations in the IO monad - -- $createSpec--- Here I discuss the creation of a 'SupervisorSpec'+-- A 'SupervisorSpec' simply holds the state of our supervision, and can be safely shared+-- between supervisors. Under the hood, both the `SupervisorSpec` and the `Supervisor`+-- share the same structure; in fact, they are  just type synonyms:+--+-- > type SupervisorSpec = Supervisor_ Uninitialised+-- > type Supervisor = Supervisor_ Initialised+-- The important difference though, is that the `SupervisorSpec` does not imply the creation+-- of an asynchronous thread, which the latter does. To keep separated the initialisation+-- of the data structure from the logic of supervising, we use GADTs and type synonyms to+-- force you create a spec first.+-- Creating a spec it just a matter of calling `newSupervisorSpec`.  -- $createSupervisor--- Here I discuss the creation of a 'Supervisor' from a 'SupervisionSpec'+-- Creating a 'Supervisor' from a 'SupervisionSpec', is as simple as calling `newSupervisor`.+-- immediately after doing so, a new thread will be started, monitoring any subsequent IO actions+-- submitted to it.  -- $supervising--- Here I discuss how you can supervise other threads.---- $conclusions--- I hope that you are now convinced that this library can be of some use to you!+-- Let's wrap everything together into a full blown example: -- -- > main :: IO () -- > main = bracketOnError (do--- >   supSpec <- newSupervisor--- >   sup <- supervise supSpec--- >   j1 <- forkSupervised sup OneForOne job1--- >   _ <- forkSupervised sup OneForOne (job2 j1)--- >   _ <- forkSupervised sup OneForOne job3--- >   _ <- forkSupervised sup OneForOne job4--- >   _ <- forkIO (go (eventStream sup))--- >   return sup) shutdownSupervisor (\_ -> threadDelay 10000000000)+-- >   supSpec <- newSupervisorSpec+-- >+-- >   sup1 <- newSupervisor supSpec+-- >   sup2 <- newSupervisor supSpec+-- >+-- >   sup1 `monitor` sup2+-- >+-- >   _ <- forkSupervised sup2 OneForOne job3+-- >+-- >   j1 <- forkSupervised sup1 OneForOne job1+-- >   _ <- forkSupervised sup1 OneForOne (job2 j1)+-- >   _ <- forkSupervised sup1 OneForOne job4+-- >   _ <- forkIO (go (eventStream sup1))+-- >   return sup1) shutdownSupervisor (\_ -> threadDelay 10000000000) -- >   where -- >    go eS = do -- >      newE <- atomically $ readTBQueue eS -- >      print newE -- >      go eS+--+-- What we have done here, was to spawn our supervisor out from a spec,+-- any using our swiss knife `forkSupervised` to spawn for supervised+-- IO computations. As you can see, if we partially apply `forkSupervised`,+-- its type resemble `forkIO` one; this is by design, as we want to keep+-- this API as IO-friendly as possible+-- in the very same example, we also create another supervisor+-- (from the same spec, but you can create a separate one as well)+-- and we ask the first supervisor to monitor the second one.+--+-- If you run this program, hopefully you should see on stdout+-- something like this:+--+-- > ChildBorn ThreadId 62 2015-02-13 11:51:15.293882 UTC+-- > ChildBorn ThreadId 63 2015-02-13 11:51:15.293897 UTC+-- > ChildBorn ThreadId 64 2015-02-13 11:51:15.293904 UTC+-- > ChildDied ThreadId 61 (MonitoredSupervision ThreadId 61) 2015-02-13 11:51:15.293941 UTC+-- > ChildBorn ThreadId 65 2015-02-13 11:51:15.294014 UTC+-- > ChildFinished ThreadId 64 2015-02-13 11:51:18.294797 UTC+-- > ChildDied ThreadId 63 thread killed 2015-02-13 11:51:18.294909 UTC+-- > ChildDied ThreadId 62 Oh boy, I'm good as dead 2015-02-13 11:51:20.294861 UTC+-- > ChildRestarted ThreadId 62 ThreadId 68 OneForOne 2015-02-13 11:51:20.294861 UTC+-- > ChildFinished ThreadId 65 2015-02-13 11:51:22.296089 UTC+-- > ChildDied ThreadId 68 Oh boy, I'm good as dead 2015-02-13 11:51:25.296189 UTC+-- > ChildRestarted ThreadId 68 ThreadId 69 OneForOne 2015-02-13 11:51:25.296189 UTC+-- > ChildDied ThreadId 69 Oh boy, I'm good as dead 2015-02-13 11:51:30.297464 UTC+-- > ChildRestarted ThreadId 69 ThreadId 70 OneForOne 2015-02-13 11:51:30.297464 UTC+-- > ChildDied ThreadId 70 Oh boy, I'm good as dead 2015-02-13 11:51:35.298123 UTC+-- > ChildRestarted ThreadId 70 ThreadId 71 OneForOne 2015-02-13 11:51:35.298123 UTC++-- $conclusions+-- I hope that you are now convinced that this library can be of some use to you!
test/Tests.hs view
@@ -15,7 +15,6 @@ import           Control.Concurrent.STM import           Control.Exception import           Control.Concurrent.Supervisor-import           Data.Time  -------------------------------------------------------------------------------- type IOProperty = PropertyM IO@@ -121,8 +120,8 @@ -- Control.Concurrent.Supervisor tests test1SupThreadNoEx :: IOProperty () test1SupThreadNoEx = forAllM randomLiveTime $ \ttl -> do-  supSpec <- lift newSupervisor-  sup <- lift $ supervise supSpec+  supSpec <- lift newSupervisorSpec+  sup <- lift $ newSupervisor supSpec   _ <- lift (forkSupervised sup OneForOne (forever $ threadDelay ttl))   assertActiveThreads sup (== 1)   lift $ shutdownSupervisor sup@@ -130,8 +129,8 @@ -------------------------------------------------------------------------------- test1SupThreadPrematureDemise :: IOProperty () test1SupThreadPrematureDemise = forAllM randomLiveTime $ \ttl -> do-  supSpec <- lift newSupervisor-  sup <- lift $ supervise supSpec+  supSpec <- lift newSupervisorSpec+  sup <- lift $ newSupervisor supSpec   tid <- lift (forkSupervised sup OneForOne (forever $ threadDelay ttl))   lift $ do     throwTo tid (AssertionFailed "You must die")@@ -165,8 +164,8 @@ -- the side effects strikes. testKillingSpree :: IOProperty () testKillingSpree = forAllM arbitrary $ \ep@(ExecutionPlan _ acts) -> do-  supSpec <- lift newSupervisor-  sup <- lift $ supervise supSpec+  supSpec <- lift newSupervisorSpec+  sup <- lift $ newSupervisor supSpec   _ <- forM acts $ lift . fromAction sup   lift (threadDelay $ maxWait acts * 2)   q <- lift $ qToList (eventStream sup)@@ -180,8 +179,8 @@ testSupCleanup :: IOProperty () testSupCleanup = forAllM (vectorOf 100 arbitrary) $ \ttls -> do   let acts = map DieAfter ttls-  supSpec <- lift newSupervisor-  sup <- lift $ supervise supSpec+  supSpec <- lift newSupervisorSpec+  sup <- lift $ newSupervisor supSpec   _ <- forM acts $ lift . fromAction sup   lift (threadDelay $ maxWait acts * 2)   q <- lift $ qToList (eventStream sup)
threads-supervisor.cabal view
@@ -1,5 +1,5 @@ name:                threads-supervisor-version:             1.0.0.0+version:             1.0.1.0 synopsis:            Simple, IO-based library for Erlang-style thread supervision description:         Simple, IO-based library for Erlang-style thread supervision license:             MIT@@ -27,6 +27,9 @@   hs-source-dirs:     src   default-language:    Haskell2010+  ghc-options:+    -Wall+    -funbox-strict-fields  test-suite threads-supervisor-tests   type:@@ -39,6 +42,9 @@     test   default-language:     Haskell2010+  ghc-options:+    -threaded+    "-with-rtsopts=-N"   build-depends:       threads-supervisor -any     , base