packages feed

massiv-scheduler 0.1.0.0 → 0.1.1.0

raw patch · 4 files changed

+139/−100 lines, 4 filesdep +unliftioPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: unliftio

API changes (from Hackage documentation)

+ Control.Massiv.Scheduler: instance GHC.Exception.Type.Exception Control.Massiv.Scheduler.WorkerTerminateException
+ Control.Massiv.Scheduler: instance GHC.Show.Show Control.Massiv.Scheduler.WorkerTerminateException
- Control.Massiv.Scheduler: traverseConcurrently :: (MonadUnliftIO m, Foldable t) => Comp -> (a -> m b) -> t a -> m [b]
+ Control.Massiv.Scheduler: traverseConcurrently :: (MonadUnliftIO m, Traversable t) => Comp -> (a -> m b) -> t a -> m (t b)
- Control.Massiv.Scheduler: traverse_ :: (Foldable t, Applicative f) => (a -> f ()) -> t a -> f ()
+ Control.Massiv.Scheduler: traverse_ :: (Applicative f, Foldable t) => (a -> f ()) -> t a -> f ()

Files

README.md view
@@ -89,12 +89,6 @@ aaaaaaaaabcdd*** Exception: divide by zero ``` -A special case is when a thread is killed by an async exception. Whenever that happens than the-exception will be re-thrown in a scheduling thread, but it will be wrapped in a custom-`WorkerAsyncException` exception. If for some reason you need to recover the original async-exception you can use `fromWorkerAsyncException`. See function documentation for an example.-- ### Nested jobs  Scheduling actions can themselves schedule actions indefinitely. That of course means that order of
massiv-scheduler.cabal view
@@ -1,5 +1,5 @@ name:                massiv-scheduler-version:             0.1.0.0+version:             0.1.1.0 synopsis:            Work stealing scheduler for Massiv (Массив) and other parallel applications. description:         A work stealing scheduler that is used by [massiv](https://www.stackage.org/package/massiv) array librarry, but can be useful for any other library or application that fits such model of computation. homepage:            https://github.com/lehins/massiv@@ -25,7 +25,7 @@    other-modules:       Control.Massiv.Scheduler.Computation                      , Control.Massiv.Scheduler.Queue-  build-depends:       base            >= 4.8 && < 5+  build-depends:       base            >= 4.9 && < 5                      , atomic-primops                      , deepseq                      , exceptions@@ -41,11 +41,12 @@   main-is:            Main.hs   other-modules:      Spec                     , Control.Massiv.SchedulerSpec-  build-depends:      base            >= 4.8 && < 5+  build-depends:      base                     , deepseq                     , massiv-scheduler                     , hspec                     , QuickCheck+                    , unliftio    default-language:   Haskell2010   ghc-options:        -Wall -fno-warn-orphans -threaded -with-rtsopts=-N
src/Control/Massiv/Scheduler.hs view
@@ -35,6 +35,7 @@ import Data.Atomics (atomicModifyIORefCAS, atomicModifyIORefCAS_) import Data.Foldable as F (foldl') import Data.IORef+import Data.Traversable  data Jobs m a = Jobs   { jobsNumWorkers :: {-# UNPACK #-} !Int@@ -51,7 +52,7 @@ -- -- @since 0.1.0 data Scheduler m a = Scheduler-  { numWorkers :: {-# UNPACK #-} !Int+  { numWorkers   :: {-# UNPACK #-} !Int   -- ^ Get the number of workers.   , scheduleWork :: m a -> m ()   -- ^ Schedule an action to be picked up and computed by a worker from a pool.@@ -60,7 +61,7 @@ -- | This is generally a faster way to traverse while ignoring the result rather than using `mapM_`. -- -- @since 0.1.0-traverse_ :: (Foldable t, Applicative f) => (a -> f ()) -> t a -> f ()+traverse_ :: (Applicative f, Foldable t) => (a -> f ()) -> t a -> f () traverse_ f = F.foldl' (\c a -> c *> f a) (pure ())  @@ -68,9 +69,17 @@ -- strategy. -- -- @since 0.1.0-traverseConcurrently :: (MonadUnliftIO m, Foldable t) => Comp -> (a -> m b) -> t a -> m [b]-traverseConcurrently comp f xs = withScheduler comp $ \s -> traverse_ (scheduleWork s . f) xs+traverseConcurrently :: (MonadUnliftIO m, Traversable t) => Comp -> (a -> m b) -> t a -> m (t b)+traverseConcurrently comp f xs = do+  ys <- withScheduler comp $ \s -> traverse_ (scheduleWork s . f) xs+  pure $ transList ys xs +transList :: Traversable t => [a] -> t b -> t a+transList xs' = snd . mapAccumL withR xs'+  where+    withR (x:xs) _ = (xs, x)+    withR _      _ = error "Impossible<traverseConcurrently> - Mismatched sizes"+ -- | Just like `traverseConcurrently`, but discard the results of computation. -- -- @since 0.1.0@@ -96,7 +105,7 @@       return res   pushJQueue jobsQueue job --- | Helper function to place `Retire` instructions on the job queue.+-- | Helper function to place required number of `Retire` instructions on the job queue. retireWorkersN :: MonadIO m => JQueue m a -> Int -> m () retireWorkersN jobsQueue n = traverse_ (pushJQueue jobsQueue) $ replicate n Retire @@ -186,11 +195,11 @@ withSchedulerInternal comp submitWork collect onScheduler = do   jobsNumWorkers <-     case comp of-      Seq -> return 1-      Par -> liftIO getNumCapabilities+      Seq      -> return 1+      Par      -> liftIO getNumCapabilities       ParOn ws -> return $ length ws-      ParN 0 -> liftIO getNumCapabilities-      ParN n -> return $ fromIntegral n+      ParN 0   -> liftIO getNumCapabilities+      ParN n   -> return $ fromIntegral n   sWorkersCounterRef <- liftIO $ newIORef jobsNumWorkers   jobsQueue <- newJQueue   jobsCountRef <- liftIO $ newIORef 0@@ -205,13 +214,12 @@   jc <- liftIO $ readIORef jobsCountRef   when (jc == 0) $ scheduleJobs_ jobs (pure ())   let spawnWorkersWith fork ws =-        forM ws $ \w ->-          withRunInIO $ \run ->-            mask_ $+        withRunInIO $ \run ->+          forM ws $ \w ->             fork w $ \unmask ->               catch                 (unmask $ run $ runWorker jobsQueue onRetire)-                (unmask . run . handleWorkerException jobsQueue workDoneMVar jobsNumWorkers)+                (run . handleWorkerException jobsQueue workDoneMVar jobsNumWorkers)       {-# INLINE spawnWorkersWith #-}       spawnWorkers =         case comp of@@ -227,76 +235,61 @@         -- \ wait for all worker to finish. If any one of them had a problem this MVar will         -- contain an exception         case mExc of-          Nothing-             -- / Now we are sure all workers have done their job we can safely read all of the-             -- IORefs with results-           -> collect jobsQueue-          Just exc -- / Here we need to unwrap the legit worker exception and rethrow it, so the-                     -- main thread will think like it's his own-            | Just (WorkerException wexc) <- fromException exc -> liftIO $ throwIO wexc-          Just exc -> liftIO $ throwIO exc -- Somethig funky is happening, propagate it.+          Nothing                    -> collect jobsQueue+          -- \ Now we are sure all workers have done their job we can safely read all of the+          -- IORefs with results+          Just (WorkerException exc) -> liftIO $ throwIO exc+          -- \ Here we need to unwrap the legit worker exception and rethrow it, so the main thread+          -- will think like it's his own       {-# INLINE doWork #-}   safeBracketOnError     spawnWorkers-    (liftIO . traverse_ (`throwTo` WorkerTerminateException))+    (liftIO . traverse_ (`throwTo` SomeAsyncException WorkerTerminateException))     (const doWork)   -- | Specialized exception handler for the work scheduler. handleWorkerException ::-  MonadIO m => JQueue m a -> MVar (Maybe SomeException) -> Int -> SomeException -> m ()+  MonadIO m => JQueue m a -> MVar (Maybe WorkerException) -> Int -> SomeException -> m () handleWorkerException jQueue workDoneMVar nWorkers exc =-  case fromException exc of+  case asyncExceptionFromException exc of     Just WorkerTerminateException -> return ()       -- \ some co-worker died, we can just move on with our death.-    _ ->-      case fromException exc of-        Just asyncExc -> do-          liftIO $ putMVar workDoneMVar $ Just $ toException $ WorkerAsyncException asyncExc-          -- \ Let the main thread know about this terrible async exception.-          -- / Do the co-worker cleanup-          retireWorkersN jQueue (nWorkers - 1)-          liftIO $ throwIO asyncExc-          -- \ do not recover from an async exception-        Nothing -> do-          liftIO $ putMVar workDoneMVar $ Just $ toException $ WorkerException exc-          -- \ Main thread must know how we died-          -- / Do the co-worker cleanup-          retireWorkersN jQueue (nWorkers - 1)-          -- / As to one self, gracefully leave off into the outer world+    _ -> do+      _ <- liftIO $ tryPutMVar workDoneMVar $ Just $ WorkerException exc+      -- \ Main thread must know how we died+      -- / Do the co-worker cleanup+      retireWorkersN jQueue (nWorkers - 1)   -- | This exception should normally be not seen in the wild. The only one that could possibly pop up -- is the `WorkerAsyncException`.-data WorkerException-  = WorkerException !SomeException+newtype WorkerException =+  WorkerException SomeException   -- ^ One of workers experienced an exception, main thread will receive the same `SomeException`.-  | WorkerAsyncException !SomeAsyncException-  -- ^ If a worker recieves an async exception, something is utterly wrong. This exception in-  -- particular would mean that one of the workers have died of an asyncronous exception. We don't-  -- want to raise that async exception synchronously in the main thread, therefore it will receive-  -- this wrapper exception instead.-  | WorkerTerminateException-  -- ^ When a brother worker dies of some exception, all the other ones will be terminated-  -- asynchronously with this one.-  deriving Show+  deriving (Show)  instance Exception WorkerException where   displayException workerExc =     case workerExc of       WorkerException exc ->         "A worker handled a job that ended with exception: " ++ displayException exc-      WorkerAsyncException exc ->-        "A worker was killed with an async exception: " ++ displayException exc-      WorkerTerminateException -> "A worker was terminated by the scheduler" +data WorkerTerminateException =+  WorkerTerminateException+  -- ^ When a brother worker dies of some exception, all the other ones will be terminated+  -- asynchronously with this one.+  deriving (Show) --- | If any one of the workers dies with an async exception, it is possible to recover that--- exception in the main thread with this function. This does not apply to `Seq`uential computation,--- since no workers are created in such case and async exception will be received by the main thread--- directly.++instance Exception WorkerTerminateException where+  displayException WorkerTerminateException = "A worker was terminated by the scheduler"+++-- | If any one of the workers dies with any exception, even the async exception, it will be+-- re-thrown in the main thread. ----- >>> let didAWorkerDie = handleJust fromWorkerAsyncException (return . (== ThreadKilled)) . fmap or+-- >>> let didAWorkerDie = handleJust asyncExceptionFromException (return . (== ThreadKilled)) . fmap or -- >>> :t didAWorkerDie -- didAWorkerDie :: Foldable t => IO (t Bool) -> IO Bool -- >>> didAWorkerDie $ withScheduler Par $ \ s -> scheduleWork s $ pure False@@ -304,14 +297,12 @@ -- >>> didAWorkerDie $ withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False -- True -- >>> withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False--- *** Exception: WorkerAsyncException thread killed+-- *** Exception: thread killed -- -- @since 0.1.0 fromWorkerAsyncException :: Exception e => SomeException -> Maybe e-fromWorkerAsyncException exc =-  case fromException exc of-    Just (WorkerAsyncException asyncExc) -> asyncExceptionFromException (toException asyncExc)-    _                                    -> Nothing+fromWorkerAsyncException = asyncExceptionFromException+{-# DEPRECATED fromWorkerAsyncException "In favor of `asyncExceptionFromException`" #-}   -- Copy from unliftio:@@ -321,7 +312,6 @@   res1 <- try $ restore $ run $ thing x   case res1 of     Left (e1 :: SomeException) -> do-      -- ignore the exception, see bracket for explanation       _ :: Either SomeException b <-         try $ uninterruptibleMask_ $ run $ after x       throwIO e1
tests/Control/Massiv/SchedulerSpec.hs view
@@ -3,13 +3,18 @@ import Control.Concurrent (killThread, myThreadId, threadDelay) import Control.Concurrent.MVar import Control.DeepSeq-import Control.Exception hiding (assert)-import Control.Exception.Base (ArithException(DivideByZero))+import qualified Control.Exception as EUnsafe+import Control.Exception.Base (ArithException(DivideByZero),+                               AsyncException(ThreadKilled)) import Control.Massiv.Scheduler+import Control.Monad import Data.List (sort) import Test.Hspec import Test.QuickCheck+import Test.QuickCheck.Function import Test.QuickCheck.Monadic+import UnliftIO.Async+import UnliftIO.Exception hiding (assert)   instance Arbitrary Comp where@@ -47,7 +52,7 @@   where     schedule [] = return []     schedule (y:ys) = do-      y' <- withScheduler comp (\s -> scheduleWork s (return y))+      y' <- withScheduler comp (`scheduleWork` pure y)       ys' <- schedule ys       return (y':ys') @@ -62,6 +67,12 @@     schedule (y:ys) =       withScheduler comp (\s -> scheduleWork s (schedule ys >>= \ys' -> return (y : concat ys'))) +prop_Traversable :: Comp -> [Int] -> Fun Int Int -> Property+prop_Traversable comp xs f =+  monadicIO $ run $ (===) <$> traverse f' xs <*> traverseConcurrently comp f' xs+  where+    f' = pure . apply f+ prop_ArbitraryCompNested :: [(Comp, Int)] -> Property prop_ArbitraryCompNested xs =   monadicIO $@@ -120,18 +131,50 @@  prop_ExpectAsyncException :: Comp -> Property prop_ExpectAsyncException comp =-  let didAWorkerDie = handleJust fromWorkerAsyncException' (return . (== ThreadKilled)) . fmap or-      fromWorkerAsyncException' =-        case comp of-          Seq -> asyncExceptionFromException-          _   -> fromWorkerAsyncException-   in (monadicIO .-       run .-       didAWorkerDie .-       withScheduler comp $ \s -> scheduleWork s (myThreadId >>= killThread >> pure False)) .&&.-      (monadicIO .-       run . fmap not . didAWorkerDie . withScheduler Par $ \s -> scheduleWork s $ pure False)+  let didAWorkerDie =+        EUnsafe.handleJust EUnsafe.asyncExceptionFromException (return . (== EUnsafe.ThreadKilled)) .+        fmap or+   in (monadicIO . run . didAWorkerDie . withScheduler comp $ \s ->+         scheduleWork s (myThreadId >>= killThread >> pure False)) .&&.+      (monadicIO . run . fmap not . didAWorkerDie . withScheduler Par $ \s ->+         scheduleWork s $ pure False) +prop_WorkerCaughtAsyncException :: Positive Int -> Property+prop_WorkerCaughtAsyncException (Positive n) =+  assertExceptionIO (== DivideByZero) $ do+    lock <- newEmptyMVar+    result <-+      race (readMVar lock) $+      withScheduler_ (ParN 2) $ \scheduler -> do+        scheduleWork scheduler $ do+          threadDelay (n `mod` 1000000)+          EUnsafe.throwIO DivideByZero+        scheduleWork scheduler $ do+          e <- tryAny $ replicateM_ 5 $ threadDelay 1000000+          case e of+            Right _ -> throwString "Impossible, shouldn't have waited for so long"+            Left exc -> do+              putMVar lock exc+              throwString $+                "I should not have survived: " ++ displayException (exc :: SomeException)+    void $ throwString $+      case result of+        Left innerError -> "Scheduled job cought async exception: " ++ displayException innerError+        Right () -> "Scheduler terminated properly. Should not have happened"++-- | Make sure there is no problems if sub-schedules worker get killed+prop_AllWorkersDied :: Comp -> Comp -> Positive Int -> Property+prop_AllWorkersDied comp1 comp (Positive n) =+  assertAsyncExceptionIO+    (== ThreadKilled)+    (withScheduler_ comp1 $ \scheduler1 ->+       scheduleWork+         scheduler1+         (withScheduler_ comp $ \scheduler ->+            replicateM_ n (scheduleWork scheduler (myThreadId >>= killThread))))+++ spec :: Spec spec = do   describe "Seq" $ do@@ -144,15 +187,20 @@     it "Recursive" $ property $ \ cs -> prop_Recursive (ParOn cs)     it "Nested" $ property $ \ cs -> prop_Nested (ParOn cs)     it "Serially" $ property $ \ cs -> prop_Serially (ParOn cs)-  describe "Arbitrary Comp" $+  describe "Arbitrary Comp" $ do     it "ArbitraryNested" $ property prop_ArbitraryCompNested+    it "traverseConcurrently == traverse" $ property prop_Traversable   describe "Exceptions" $ do     it "CatchDivideByZero" $ property prop_CatchDivideByZero     it "CatchDivideByZeroNested" $ property prop_CatchDivideByZeroNested     it "KillBlockedCoworker" $ property prop_KillBlockedCoworker     it "KillSleepingCoworker" $ property prop_KillSleepingCoworker     it "ExpectAsyncException" $ property prop_ExpectAsyncException+    it "WorkerCaughtAsyncException" $ property prop_WorkerCaughtAsyncException+    it "AllWorkersDied" $ property prop_AllWorkersDied ++-- | Assert a synchronous exception assertExceptionIO :: (NFData a, Exception exc) =>                      (exc -> Bool) -- ^ Return True if that is the exception that was expected                   -> IO a -- ^ IO Action that should throw an exception@@ -160,17 +208,23 @@ assertExceptionIO isExc action =   monadicIO $ do     hasFailed <--      run-        (catch-           (do res <- action-               res `deepseq` return False) $ \exc ->-           displayException exc `deepseq` return (isExc exc))+      run $+      catch+        (do res <- action+            res `deepseq` return False) $ \exc -> displayException exc `deepseq` return (isExc exc)     assert hasFailed -------+assertAsyncExceptionIO :: (Exception e, NFData a) => (e -> Bool) -> IO a -> Property+assertAsyncExceptionIO isAsyncExc action =+  monadicIO $ do+    hasFailed <-+      run $+      EUnsafe.catch+        (do res <- action+            res `deepseq` return False)+        (\exc ->+           case EUnsafe.asyncExceptionFromException exc of+             Just asyncExc+               | isAsyncExc asyncExc -> displayException asyncExc `deepseq` pure True+             _ -> EUnsafe.throwIO exc)+    assert hasFailed