packages feed

threads-supervisor (empty) → 1.0.0.0

raw patch · 7 files changed

+576/−0 lines, 7 filesdep +QuickCheckdep +basedep +bytestringsetup-changed

Dependencies added: QuickCheck, base, bytestring, stm, tasty, tasty-hunit, tasty-quickcheck, threads-supervisor, time, transformers, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Alfredo Di Napoli++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Control/Concurrent/Supervisor.hs view
@@ -0,0 +1,167 @@++{-+  Humble module inspired to Erlang supervisors,+  with minimal dependencies.+-}++{-# LANGUAGE GADTs #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Control.Concurrent.Supervisor +  ( Supervisor+  , DeadLetter+  , Child(..)+  , RestartAction+  , SupervisionEvent(..)+  , RestartStrategy(..)+  , newSupervisor+  , shutdownSupervisor+  , eventStream+  , activeChildren+  , supervise+  , forkSupervised+  ) where++import qualified Data.HashMap.Strict as Map+import           Control.Concurrent+import           Control.Concurrent.STM+import           Data.IORef+import           Control.Exception+import           Control.Monad+import           Data.Time++--------------------------------------------------------------------------------+data Uninitialised+data Initialised++--------------------------------------------------------------------------------+data Supervisor_ a where+     NewSupervisor :: {+        _ns_myTid    :: !(Maybe ThreadId)+      , _ns_children :: !(IORef (Map.HashMap ThreadId Child))+      , _ns_mailbox :: TQueue DeadLetter+      , _ns_eventStream :: TBQueue SupervisionEvent+      } -> Supervisor_ Uninitialised+     Supervisor :: {+        _sp_myTid    :: !(Maybe ThreadId)+      , _sp_children :: !(IORef (Map.HashMap ThreadId Child))+      , _sp_mailbox :: TQueue DeadLetter+      , _sp_eventStream :: TBQueue SupervisionEvent+      } -> Supervisor_ Initialised++type SupervisorSpec = Supervisor_ Uninitialised+type Supervisor = Supervisor_ Initialised++--------------------------------------------------------------------------------+data DeadLetter = DeadLetter ThreadId SomeException++--------------------------------------------------------------------------------+data Child = Child !RestartStrategy RestartAction++--------------------------------------------------------------------------------+type RestartAction = ThreadId -> IO ThreadId++--------------------------------------------------------------------------------+data SupervisionEvent =+     ChildBorn !ThreadId !UTCTime+   | ChildDied !ThreadId !SomeException !UTCTime+   | ChildRestarted !ThreadId !ThreadId !RestartStrategy !UTCTime+   | ChildFinished !ThreadId !UTCTime+   deriving Show++--------------------------------------------------------------------------------+-- | Erlang inspired strategies.+data RestartStrategy =+     OneForOne+     deriving Show++--------------------------------------------------------------------------------+-- | Creates a new supervisor+newSupervisor :: IO SupervisorSpec+newSupervisor = do+  tkn <- newTQueueIO+  evt <- newTBQueueIO 1000+  ref <- newIORef Map.empty+  return $ NewSupervisor Nothing ref tkn evt++--------------------------------------------------------------------------------+eventStream :: Supervisor -> TBQueue SupervisionEvent+eventStream (Supervisor _ _ _ e) = e++--------------------------------------------------------------------------------+activeChildren :: Supervisor -> IO Int+activeChildren (Supervisor _ chRef _ _) = do+  readIORef chRef >>= return . length . Map.keys++--------------------------------------------------------------------------------+-- | Shutdown the given supervisor. This will cause the supervised children to+-- be killed as well.+shutdownSupervisor :: Supervisor -> IO ()+shutdownSupervisor (Supervisor sId chRef _ _) = do+  case sId of +    Nothing -> return ()+    Just tid -> do+      readIORef chRef >>= \chMap -> forM_ (Map.keys chMap) killThread+      killThread tid++--------------------------------------------------------------------------------+-- | Fork a thread in a supervised mode.+forkSupervised :: Supervisor+               -- ^ The 'Supervisor'+               -> RestartStrategy+               -- ^ The 'RestartStrategy' to use+               -> IO ()+               -- ^ The computation to run+               -> IO ThreadId+forkSupervised sup@Supervisor{..} str act =+  bracket (supervised sup act) return $ \newChild -> do+    let ch = Child str (const (supervised sup act))+    atomicModifyIORef' _sp_children $ \chMap -> (Map.insert newChild ch chMap, ())+    now <- getCurrentTime+    writeIfNotFull _sp_eventStream (ChildBorn newChild now)+    return newChild++--------------------------------------------------------------------------------+writeIfNotFull :: TBQueue SupervisionEvent -> SupervisionEvent -> IO ()+writeIfNotFull q evt = atomically $ do+  isFull <- isFullTBQueue q+  unless isFull $ writeTBQueue q evt++--------------------------------------------------------------------------------+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)+  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
+ src/Control/Concurrent/Supervisor/Tutorial.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++{-| Use @threads-supervisor@ if you want the "poor-man's Erlang supervisors".+    @threads-supervisor@ is an IO-based library  with minimal dependencies+    which does only one thing: It provides you a 'Supervisor' entity  you can use+    to monitor your forked computations. If one of the managed threads dies,+    you can decide if and how to restart it. This gives you:++    * Protection against silent exceptions which might terminate your workers.+    * A simple but powerful way of structure your program into a supervision tree,+    where the leaves are the worker threads, and the nodes can be other+    supervisors being monitored.+    * A disaster recovery mechanism.++  You can install the @threads-supervisor@ library by running:+  > $ cabal install threads-supervisor+-}++module Control.Concurrent.Supervisor.Tutorial+ (  -- * Introduction+    -- $introduction++    -- * Different type of jobs+    -- $jobs++    -- * Creating a SupervisorSpec+    -- $createSpec++    -- * Creating a Supervisor+    -- $createSupervisor++    -- * Supervising and choosing a 'RestartStrategy'+    -- $supervising++    -- * Wrapping up+    -- $conclusions++ ) where++-- $introduction+-- Who worked with Haskell's concurrency primitives would be surely familiar+-- with the `forkIO` function, which allow us to fork an IO computation in a separate+-- green thread. `forkIO` is great, but is also very low level, and has a+-- couple of subtleties, as you can read from this passage of the documentation:+--+--    The newly created thread has an exception handler that discards the exceptions +--    `BlockedIndefinitelyOnMVar`,`BlockedIndefinitelyOnSTM`, and `ThreadKilled`, +--    and passes all other exceptions to the uncaught exception handler.+--+-- To mitigate this, we have a couple of libraries available,  for example+-- <http://hackage.haskell.org/package/async> and <http://hackage.haskell.org/package/slave-thread>.+-- +-- But what about if I do not want to take explicit action, but instead specifying upfront+-- how to react to disaster, and leave the library work out the details?+-- This is what this library aims to do.++-- $jobs+-- In this example, let's create four different threads:+--+-- > job1 :: IO ()+-- > job1 = do+-- >   threadDelay 5000000+-- >   fail "Dead"+-- This job will die after five seconds.+--+-- > job2 :: ThreadId -> IO ()+-- > job2 tid = do+-- >   threadDelay 3000000+-- >   killThread tid+--+-- This other job instead, we have waited three seconds, and then kill a target thread,+-- generating an asynchronous exception.+--+-- > job3 :: IO ()+-- > job3 = do+-- >   threadDelay 5000000+-- >   error "Oh boy, I'm good as dead"+--+-- This guy is very similar to the first one, except for the fact `error` is used instead of `fail`.+--+-- > job4 :: IO ()+-- > job4 = threadDelay 7000000+-- @job4@ is what we wish for all our passing cross computation: smooth sailing.+--+-- These jobs represent a significant pool of our everyday computations in the IO monad+++-- $createSpec+-- Here I discuss the creation of a 'SupervisorSpec'++-- $createSupervisor+-- Here I discuss the creation of a 'Supervisor' from a 'SupervisionSpec'++-- $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!+--+-- > 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)+-- >   where+-- >    go eS = do+-- >      newE <- atomically $ readTBQueue eS+-- >      print newE+-- >      go eS
+ test/Main.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           Test.Tasty+import           Test.Tasty.QuickCheck+import           Test.QuickCheck.Monadic+import           Tests+++--------------------------------------------------------------------------------+main :: IO ()+main = defaultMain allTests+++--------------------------------------------------------------------------------+withQuickCheckDepth :: TestName -> Int -> [TestTree] -> TestTree+withQuickCheckDepth tn depth tests =+  localOption (QuickCheckTests depth) (testGroup tn tests)+++--------------------------------------------------------------------------------+allTests :: TestTree+allTests = testGroup "All Tests" [+    withQuickCheckDepth "Control.Concurrent.Supervisor" 10 [+        testProperty "1 supervised thread, no exceptions" (monadicIO test1SupThreadNoEx)+      , testProperty "1 supervised thread, premature exception" (monadicIO test1SupThreadPrematureDemise)+      , testProperty "killing spree" (monadicIO testKillingSpree)+      , testProperty "cleanup" (monadicIO testSupCleanup)+    ]+  ]
+ test/Tests.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Tests where++import           Test.Tasty.HUnit as HUnit+import           Test.Tasty.QuickCheck+import           Test.QuickCheck.Monadic as QM+import qualified Data.List as List+import           Control.Monad+import           Control.Monad.Trans.Class+import           Control.Applicative+import           Control.Concurrent+import           Control.Concurrent.STM+import           Control.Exception+import           Control.Concurrent.Supervisor+import           Data.Time++--------------------------------------------------------------------------------+type IOProperty = PropertyM IO++-- How much a thread will live.+newtype TTL = TTL Int deriving Show++-- | Generate a random thread live time between 0.5 sec and 2 secs.+randomLiveTime :: Gen Int+randomLiveTime = choose (500000, 2000000)++instance Arbitrary TTL where+  arbitrary = TTL <$> randomLiveTime++data ThreadAction =+    Live+  | DieAfter TTL --natural death+  | ThrowAfter TTL+  deriving Show++instance Arbitrary ThreadAction where+  arbitrary = do+    act <- elements [const Live, DieAfter, ThrowAfter]+    ttl <- arbitrary+    return $ act ttl++-- We cannot easily deal with async exceptions+-- being thrown at us.+data ExecutionPlan = ExecutionPlan {+    toSpawn :: Int+  , actions :: [ThreadAction]+  } deriving Show++instance Arbitrary ExecutionPlan where+  arbitrary = do+    ts <- choose (1,20)+    acts <- vectorOf ts arbitrary+    return $ ExecutionPlan ts acts++--------------------------------------------------------------------------------+howManyRestarted :: ExecutionPlan -> Int+howManyRestarted (ExecutionPlan _ acts) = length . filter pred_ $ acts+  where+    pred_ (ThrowAfter _) = True+    pred_ _ = False++--------------------------------------------------------------------------------+howManyLiving :: ExecutionPlan -> Int+howManyLiving (ExecutionPlan _ acts) = length . filter pred_ $ acts+  where+    pred_ Live = True+    pred_ _ = False++--------------------------------------------------------------------------------+assertActiveThreads :: Supervisor -> (Int -> Bool) -> IOProperty ()+assertActiveThreads sup p = do+  ac <- lift (activeChildren sup)+  QM.assert (p ac)++--------------------------------------------------------------------------------+qToList :: TBQueue SupervisionEvent -> IO [SupervisionEvent]+qToList q = do+  nextEl <- atomically (tryReadTBQueue q)+  case nextEl of+    (Just el) -> (el :) <$> qToList q+    Nothing -> return []++--------------------------------------------------------------------------------+assertContainsNMsg :: (SupervisionEvent -> Bool) +                   -> Int+                   -> [SupervisionEvent] +                   -> IOProperty ()+assertContainsNMsg _ 0 _ = QM.assert True+assertContainsNMsg _ x [] = lift $+  HUnit.assertBool ("assertContainsNMsg: list exhausted and " ++ show x ++ " left.") False+assertContainsNMsg matcher !n (x:xs) = case matcher x of+  True  -> assertContainsNMsg matcher (n - 1) xs+  False -> assertContainsNMsg matcher n xs++--------------------------------------------------------------------------------+assertContainsNRestartMsg :: Int -> [SupervisionEvent] -> IOProperty ()+assertContainsNRestartMsg = assertContainsNMsg matches+  where+    matches (ChildRestarted{}) = True+    matches _ = False++--------------------------------------------------------------------------------+assertContainsNFinishedMsg :: Int -> [SupervisionEvent] -> IOProperty ()+assertContainsNFinishedMsg = assertContainsNMsg matches+  where+    matches (ChildFinished{}) = True+    matches _ = False++--------------------------------------------------------------------------------+assertContainsRestartMsg :: [SupervisionEvent] -> ThreadId -> IOProperty ()+assertContainsRestartMsg [] _ = QM.assert False+assertContainsRestartMsg (x:xs) tid = case x of+  ((ChildRestarted old _ _ _)) -> +    if old == tid then QM.assert True else assertContainsRestartMsg xs tid+  _ -> assertContainsRestartMsg xs tid++--------------------------------------------------------------------------------+-- Control.Concurrent.Supervisor tests+test1SupThreadNoEx :: IOProperty ()+test1SupThreadNoEx = forAllM randomLiveTime $ \ttl -> do+  supSpec <- lift newSupervisor+  sup <- lift $ supervise supSpec+  _ <- lift (forkSupervised sup OneForOne (forever $ threadDelay ttl))+  assertActiveThreads sup (== 1)+  lift $ shutdownSupervisor sup++--------------------------------------------------------------------------------+test1SupThreadPrematureDemise :: IOProperty ()+test1SupThreadPrematureDemise = forAllM randomLiveTime $ \ttl -> do+  supSpec <- lift newSupervisor+  sup <- lift $ supervise supSpec+  tid <- lift (forkSupervised sup OneForOne (forever $ threadDelay ttl))+  lift $ do+    throwTo tid (AssertionFailed "You must die")+    threadDelay ttl --give time to restart the thread+  assertActiveThreads sup (== 1)+  q <- lift $ qToList (eventStream sup)+  assertContainsNRestartMsg 1 q+  lift $ shutdownSupervisor sup++--------------------------------------------------------------------------------+fromAction :: Supervisor -> ThreadAction -> IO ThreadId+fromAction s Live = forkSupervised s OneForOne (forever $ threadDelay 100000000)+fromAction s (DieAfter (TTL ttl)) = forkSupervised s OneForOne (threadDelay ttl)+fromAction s (ThrowAfter (TTL ttl)) = forkSupervised s OneForOne (do+  threadDelay ttl +  throwIO $ AssertionFailed "die")++--------------------------------------------------------------------------------+maxWait :: [ThreadAction] -> Int+maxWait ta = go ta []+  where+    go [] [] = 0+    go [] acc = List.maximum acc+    go (Live:xs) acc = go xs acc+    go ((DieAfter (TTL t)):xs) acc = go xs (t : acc)+    go ((ThrowAfter (TTL t)):xs) acc = go xs (t : acc)++--------------------------------------------------------------------------------+-- In this test, we generate random IO actions for the threads to be+-- executed, then we calculate how many of them needs to be alive after all+-- the side effects strikes.+testKillingSpree :: IOProperty ()+testKillingSpree = forAllM arbitrary $ \ep@(ExecutionPlan _ acts) -> do+  supSpec <- lift newSupervisor+  sup <- lift $ supervise supSpec+  _ <- forM acts $ lift . fromAction sup+  lift (threadDelay $ maxWait acts * 2)+  q <- lift $ qToList (eventStream sup)+  assertActiveThreads sup (>= howManyLiving ep)+  assertContainsNRestartMsg (howManyRestarted ep) q+  lift $ shutdownSupervisor sup++--------------------------------------------------------------------------------+-- In this test, we test that the supervisor does not leak memory by removing+-- children who finished+testSupCleanup :: IOProperty ()+testSupCleanup = forAllM (vectorOf 100 arbitrary) $ \ttls -> do+  let acts = map DieAfter ttls+  supSpec <- lift newSupervisor+  sup <- lift $ supervise supSpec+  _ <- forM acts $ lift . fromAction sup+  lift (threadDelay $ maxWait acts * 2)+  q <- lift $ qToList (eventStream sup)+  assertActiveThreads sup (== 0)+  assertContainsNFinishedMsg (length acts) q+  lift $ shutdownSupervisor sup
+ threads-supervisor.cabal view
@@ -0,0 +1,52 @@+name:                threads-supervisor+version:             1.0.0.0+synopsis:            Simple, IO-based library for Erlang-style thread supervision+description:         Simple, IO-based library for Erlang-style thread supervision+license:             MIT+license-file:        LICENSE+author:              Alfredo Di Napoli+maintainer:          alfredo.dinapoli@gmail.com+copyright:           Alfredo Di Napoli+category:            Concurrency+build-type:          Simple+cabal-version:       >=1.10++source-repository head+  type: git+  location: https://github.com/adinapoli/threads-supervisor++library+  exposed-modules:+    Control.Concurrent.Supervisor+    Control.Concurrent.Supervisor.Tutorial+  build-depends:+    base                 >= 4.6 && < 5,+    unordered-containers >= 0.2.0.0 && < 0.3.0.0,+    stm                  >= 2.4,+    time                 >= 1.2+  hs-source-dirs:+    src+  default-language:    Haskell2010++test-suite threads-supervisor-tests+  type:+    exitcode-stdio-1.0+  main-is:+    Main.hs+  other-modules:+    Tests+  hs-source-dirs:+    test+  default-language:+    Haskell2010+  build-depends:+      threads-supervisor -any+    , base+    , bytestring+    , QuickCheck+    , tasty >= 0.9.0.1+    , tasty-quickcheck+    , tasty-hunit+    , time+    , stm+    , transformers