slave-thread 1.1.0.2 → 1.1.0.3
raw patch · 5 files changed
+324/−275 lines, 5 filesdep −QuickCheckdep −quickcheck-instancesdep −tasty-quickcheckdep ~stm-containers
Dependencies removed: QuickCheck, quickcheck-instances, tasty-quickcheck
Dependency ranges changed: stm-containers
Files
- library/SlaveThread.hs +34/−46
- library/SlaveThread/Prelude.hs +16/−21
- library/SlaveThread/Util/LowLevelForking.hs +0/−1
- slave-thread.cabal +116/−35
- test/Main.hs +158/−172
library/SlaveThread.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} -- | -- Vanilla thread management in Haskell is low level and -- it does not approach the problems related to thread deaths.@@ -33,25 +32,24 @@ -- and lets you be sure of getting informed -- if your program gets brought to an erroneous state. module SlaveThread-(- fork,- forkWithUnmask,- forkFinally,- forkFinallyWithUnmask,- SlaveThreadCrashed(..)- -- * Notes- -- $note-unmask-)+ ( fork,+ forkWithUnmask,+ forkFinally,+ forkFinallyWithUnmask,+ SlaveThreadCrashed (..),++ -- * Notes+ -- $note-unmask+ ) where +import qualified Control.Foldl as Foldl+import qualified DeferredFolds.UnfoldlM as UnfoldlM+import qualified Focus import SlaveThread.Prelude import SlaveThread.Util.LowLevelForking-import qualified DeferredFolds.UnfoldlM as UnfoldlM import qualified StmContainers.Multimap as Multimap-import qualified Control.Foldl as Foldl-import qualified Focus - -- | -- A global registry of all slave threads by their masters. {-# NOINLINE slaveRegistry #-}@@ -61,7 +59,7 @@ -- | -- Fork a slave thread to run a computation on.-{-# INLINABLE fork #-}+{-# INLINEABLE fork #-} fork :: IO a -> IO ThreadId fork = forkFinally $ return ()@@ -69,7 +67,7 @@ -- | -- Like 'fork', but provides the computation a function that unmasks -- asynchronous exceptions. See @Note [Unmask]@ at the bottom of this module.-{-# INLINABLE forkWithUnmask #-}+{-# INLINEABLE forkWithUnmask #-} forkWithUnmask :: ((forall x. IO x -> IO x) -> IO a) -> IO ThreadId forkWithUnmask = forkFinallyWithUnmask $ return ()@@ -82,7 +80,7 @@ -- Note the order of arguments: -- -- >forkFinally finalizer computation-{-# INLINABLE forkFinally #-}+{-# INLINEABLE forkFinally #-} forkFinally :: IO a -> IO b -> IO ThreadId forkFinally finalizer computation = forkFinallyWithUnmask finalizer (\unmask -> unmask computation)@@ -90,44 +88,42 @@ -- | -- Like 'forkFinally', but provides the computation a function that unmasks -- asynchronous exceptions. See @Note [Unmask]@ at the bottom of this module.-{-# INLINABLE forkFinallyWithUnmask #-}+{-# INLINEABLE forkFinallyWithUnmask #-} forkFinallyWithUnmask :: IO a -> ((forall x. IO x -> IO x) -> IO b) -> IO ThreadId forkFinallyWithUnmask finalizer computation = uninterruptibleMask $ \unmask -> do- masterThread <- myThreadId slaveThread <- forkIOWithoutHandler $ do- slaveThread <- myThreadId -- Execute the main computation: computationExceptions <- catch (computation unmask $> empty) (return . pure) -- Kill the slaves and wait for them to die:- slavesDyingExceptions <- let- loop !exceptions =- catch- (unmask $ do- killSlaves slaveThread- waitForSlavesToDie slaveThread- return exceptions)- (\ !exception -> loop (exception : exceptions))- in loop []+ slavesDyingExceptions <-+ let loop !exceptions =+ catch+ ( unmask $ do+ killSlaves slaveThread+ waitForSlavesToDie slaveThread+ return exceptions+ )+ (\ !exception -> loop (exception : exceptions))+ in loop [] -- Finalize: finalizerExceptions <- catch (finalizer $> empty) (return . pure) -- Rethrow the exceptions:- let- handler e = do- case fromException e of- Just ThreadKilled -> return ()- _ -> throwTo masterThread (SlaveThreadCrashed slaveThread e)- in do- forM_ @Maybe computationExceptions handler- forM_ slavesDyingExceptions handler- forM_ @Maybe finalizerExceptions handler+ let handler e = do+ case fromException e of+ Just ThreadKilled -> return ()+ _ -> throwTo masterThread (SlaveThreadCrashed slaveThread e)+ in do+ forM_ @Maybe computationExceptions handler+ forM_ slavesDyingExceptions handler+ forM_ @Maybe finalizerExceptions handler -- Unregister from the global state, -- thus informing the master of this thread's death.@@ -144,21 +140,13 @@ killSlaves :: ThreadId -> IO () killSlaves thread = do-#if MIN_VERSION_stm_containers(1,2,0) threads <- atomically (UnfoldlM.foldM (Foldl.generalize Foldl.revList) (Multimap.unfoldlMByKey thread slaveRegistry))-#else- threads <- atomically (UnfoldlM.foldM (Foldl.generalize Foldl.revList) (Multimap.unfoldMByKey thread slaveRegistry))-#endif traverse_ killThread threads waitForSlavesToDie :: ThreadId -> IO () waitForSlavesToDie thread = atomically $ do-#if MIN_VERSION_stm_containers(1,2,0) null <- UnfoldlM.null $ Multimap.unfoldlMByKey thread slaveRegistry-#else- null <- UnfoldlM.null $ Multimap.unfoldMByKey thread slaveRegistry-#endif unless null retry -- | A slave thread crashed. This exception is classified as /asynchronous/,
library/SlaveThread/Prelude.hs view
@@ -1,19 +1,16 @@ module SlaveThread.Prelude-(- module Exports,-)+ ( module Exports,+ ) where --- base-------------------------- import Control.Applicative as Exports import Control.Arrow as Exports import Control.Category as Exports import Control.Concurrent as Exports hiding (forkFinally) import Control.Exception as Exports-import Control.Monad as Exports hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)-import Control.Monad.IO.Class as Exports+import Control.Monad as Exports hiding (forM, forM_, mapM, mapM_, msum, sequence, sequence_) import Control.Monad.Fix as Exports hiding (fix)+import Control.Monad.IO.Class as Exports import Control.Monad.ST as Exports import Data.Bits as Exports import Data.Bool as Exports@@ -26,19 +23,19 @@ import Data.Fixed as Exports import Data.Foldable as Exports hiding (toList) import Data.Function as Exports hiding (id, (.))-import Data.Functor as Exports+import Data.Functor as Exports hiding (unzip) import Data.Functor.Identity as Exports-import Data.Int as Exports import Data.IORef as Exports+import Data.Int as Exports import Data.Ix as Exports-import Data.List as Exports hiding (sortOn, isSubsequenceOf, uncons, concat, foldr, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, find, maximumBy, minimumBy, mapAccumL, mapAccumR, foldl')+import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons) import Data.Maybe as Exports-import Data.Monoid as Exports hiding (Last(..), First(..), (<>))+import Data.Monoid as Exports hiding (First (..), Last (..), (<>)) import Data.Ord as Exports import Data.Proxy as Exports import Data.Ratio as Exports-import Data.Semigroup as Exports import Data.STRef as Exports+import Data.Semigroup as Exports import Data.String as Exports import Data.Traversable as Exports import Data.Tuple as Exports@@ -49,14 +46,13 @@ import Foreign.ForeignPtr as Exports import Foreign.Ptr as Exports import Foreign.StablePtr as Exports-import Foreign.Storable as Exports hiding (sizeOf, alignment)-import GHC.Conc as Exports hiding (withMVar, threadWaitWriteSTM, threadWaitWrite, threadWaitReadSTM, threadWaitRead)-import GHC.Exts as Exports (lazy, inline, sortWith, groupWith, IsList(..), Int(I#), fork#, forkOn#)+import Foreign.Storable as Exports hiding (alignment, sizeOf)+import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)+import GHC.Exts as Exports (Int (I#), IsList (..), fork#, forkOn#, groupWith, inline, lazy, sortWith) import GHC.Generics as Exports (Generic)-import GHC.IO as Exports (IO(IO), unsafeUnmask)+import GHC.IO as Exports (IO (IO), unsafeUnmask) import GHC.IO.Exception as Exports import Numeric as Exports-import Prelude as Exports hiding (concat, foldr, mapM_, sequence_, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, mapM, sequence, id, (.)) import System.Environment as Exports import System.Exit as Exports import System.IO as Exports@@ -65,8 +61,7 @@ import System.Mem as Exports import System.Mem.StableName as Exports import System.Timeout as Exports-import Text.ParserCombinators.ReadP as Exports (ReadP, ReadS, readP_to_S, readS_to_P)-import Text.ParserCombinators.ReadPrec as Exports (ReadPrec, readPrec_to_P, readP_to_Prec, readPrec_to_S, readS_to_Prec)-import Text.Printf as Exports (printf, hPrintf)-import Text.Read as Exports (Read(..), readMaybe, readEither)+import Text.Printf as Exports (hPrintf, printf)+import Text.Read as Exports (Read (..), readEither, readMaybe) import Unsafe.Coerce as Exports+import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
library/SlaveThread/Util/LowLevelForking.hs view
@@ -2,7 +2,6 @@ import SlaveThread.Prelude - -- | -- A more efficient version of 'forkIO', -- which does not install a default exception handler on the forked thread.
slave-thread.cabal view
@@ -1,6 +1,8 @@-name: slave-thread-version: 1.1.0.2-synopsis: A fundamental solution to ghost threads and silent exceptions+name: slave-thread+version: 1.1.0.3+synopsis:+ A fundamental solution to ghost threads and silent exceptions+ description: Vanilla thread management in Haskell is low level and it does not approach the problems related to thread deaths.@@ -34,48 +36,127 @@ This protects you from silent exceptions and lets you be sure of getting informed if your program gets brought to an erroneous state.-category: Concurrency, Concurrent, Error Handling, Exceptions, Failure-homepage: https://github.com/nikita-volkov/slave-thread-bug-reports: https://github.com/nikita-volkov/slave-thread/issues-author: Nikita Volkov <nikita.y.volkov@mail.ru>-maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>-copyright: (c) 2014, Nikita Volkov-license: MIT-license-file: LICENSE-build-type: Simple++category:+ Concurrency, Concurrent, Error Handling, Exceptions, Failure++homepage: https://github.com/nikita-volkov/slave-thread+bug-reports: https://github.com/nikita-volkov/slave-thread/issues+author: Nikita Volkov <nikita.y.volkov@mail.ru>+maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>+copyright: (c) 2014, Nikita Volkov+license: MIT+license-file: LICENSE+build-type: Simple cabal-version: >=1.10 source-repository head- type: git+ type: git location: git://github.com/nikita-volkov/slave-thread.git library- hs-source-dirs: library- default-extensions: Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, PatternSynonyms, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples- default-language: Haskell2010- exposed-modules: SlaveThread+ hs-source-dirs: library+ default-extensions:+ NoImplicitPrelude+ NoMonomorphismRestriction+ Arrows+ BangPatterns+ ConstraintKinds+ DataKinds+ DefaultSignatures+ DeriveDataTypeable+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveTraversable+ EmptyDataDecls+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ LambdaCase+ LiberalTypeSynonyms+ MagicHash+ MultiParamTypeClasses+ MultiWayIf+ OverloadedStrings+ ParallelListComp+ PatternGuards+ PatternSynonyms+ QuasiQuotes+ RankNTypes+ RecordWildCards+ ScopedTypeVariables+ StandaloneDeriving+ TemplateHaskell+ TupleSections+ TypeApplications+ TypeFamilies+ TypeOperators+ UnboxedTuples++ default-language: Haskell2010+ exposed-modules: SlaveThread other-modules: SlaveThread.Prelude SlaveThread.Util.LowLevelForking+ build-depends:- base >=4.9 && <5,- deferred-folds >=0.9 && <0.10,- focus >=1 && <1.1,- foldl >=1 && <2,- stm-containers >=1.1 && <1.3+ base >=4.9 && <5+ , deferred-folds >=0.9 && <0.10+ , focus >=1 && <1.1+ , foldl >=1 && <2+ , stm-containers >=1.2 && <1.3 test-suite test- type: exitcode-stdio-1.0- hs-source-dirs: test- default-extensions: Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, PatternSynonyms, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples- default-language: Haskell2010- main-is: Main.hs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ default-extensions:+ NoImplicitPrelude+ NoMonomorphismRestriction+ Arrows+ BangPatterns+ ConstraintKinds+ DataKinds+ DefaultSignatures+ DeriveDataTypeable+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveTraversable+ EmptyDataDecls+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ GADTs+ GeneralizedNewtypeDeriving+ LambdaCase+ LiberalTypeSynonyms+ MagicHash+ MultiParamTypeClasses+ MultiWayIf+ OverloadedStrings+ ParallelListComp+ PatternGuards+ PatternSynonyms+ QuasiQuotes+ RankNTypes+ RecordWildCards+ ScopedTypeVariables+ StandaloneDeriving+ TemplateHaskell+ TupleSections+ TypeApplications+ TypeFamilies+ TypeOperators+ UnboxedTuples++ default-language: Haskell2010+ main-is: Main.hs build-depends:- QuickCheck >=2.8.1 && <3,- quickcheck-instances >=0.3.11 && <0.4,- rerebase <2,- SafeSemaphore ==0.10.*,- slave-thread,- tasty >=0.12 && <2,- tasty-hunit >=0.9 && <0.11,- tasty-quickcheck >=0.9 && <0.11+ rerebase <2+ , SafeSemaphore >=0.10 && <0.11+ , slave-thread+ , tasty >=0.12 && <2+ , tasty-hunit >=0.9 && <0.11
test/Main.hs view
@@ -1,189 +1,175 @@ module Main where -import Prelude-import Data.List (isInfixOf)-import Data.Either (isLeft)-import Test.QuickCheck.Instances+import qualified Control.Concurrent.SSem as SSem+import qualified SlaveThread as S import Test.Tasty-import Test.Tasty.Runners import Test.Tasty.HUnit-import Test.Tasty.QuickCheck-import qualified Test.QuickCheck as QuickCheck-import qualified Test.QuickCheck.Property as QuickCheck-import qualified SlaveThread as S-import qualified Control.Concurrent.SSem as SSem-+import Prelude +main :: IO () main =- defaultMain $- testGroup "All" $ [- testCase "Failing in finalizer doesn't break everything" $ do- finalizer1CalledVar <- newTVarIO False- finalizer2CalledVar <- newTVarIO False- result <- let- finalizer1 =- atomically $ writeTVar finalizer1CalledVar True- finalizer2 =- do- atomically $ writeTVar finalizer2CalledVar True- throwIO (userError "finalizer2 failed")- in try @SomeException $ do- S.forkFinally finalizer1 $ do- S.forkFinally finalizer2 $ threadDelay 100- threadDelay (10^4)- finalizer2Called <- atomically (readTVar finalizer2CalledVar)- finalizer1Called <- atomically (readTVar finalizer1CalledVar)- assertEqual "finalizer2 not called" True finalizer2Called- assertEqual "finalizer1 not called" True finalizer1Called- assertEqual "result is left" True (isLeft result)- assertEqual "Invalid result" True ("finalizer2 failed" `isInfixOf` show result)- ,- testCase "Forked threads run fine" $ do- replicateM_ 100000 $ do- var <- newMVar 0- let increment = modifyMVar_ var (return . succ)- semaphore <- SSem.new 0- S.fork $ do- increment- semaphore' <- SSem.new (-1)- S.fork $ do- increment- SSem.signal semaphore'- S.fork $ do- increment- SSem.signal semaphore'- SSem.wait semaphore'- SSem.signal semaphore- SSem.wait semaphore- assertEqual "" 3 =<< readMVar var- ,- testCase "Killing a thread kills deep slaves" $ do- replicateM_ 100000 $ do- var <- newMVar 0- semaphore <- SSem.new 0- thread <-- S.forkFinally (SSem.signal semaphore) $ do- join $ forkWait $ do- join $ forkWait $ do- w <- forkWait $ do- threadDelay $ 10^6- modifyMVar_ var (return . succ)- threadDelay $ 10^6- modifyMVar_ var (return . succ)- w- killThread thread- SSem.wait semaphore- assertEqual "" 0 =<< readMVar var- ,- testCase "Dying normally kills slaves" $ do- replicateM_ 100000 $ do- var <- newIORef 0- let increment = modifyIORef var (+1)- semaphore <- SSem.new 0- S.forkFinally (SSem.signal semaphore) $ do- S.fork $ do- threadDelay $ 10^6- increment- S.fork $ do- threadDelay $ 10^6- increment- SSem.wait semaphore- assertEqual "" 0 =<< readIORef var- ,- testCase "Finalization is in order" $ do- replicateM_ 100000 $ do- var <- newMVar []- semaphore <- SSem.new 0- S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (1:)) >> SSem.signal semaphore)) $ do- semaphore' <- SSem.new 0- S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (2:)) >> SSem.signal semaphore')) $ do- S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3:)))) $ return ()- S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3:)))) $ return ()- SSem.wait semaphore'- SSem.wait semaphore- assertEqual "" [1,2,3,3] =<< readMVar var- ,- testCase "Exceptions don't get lost" $ do- replicateM_ 100000 $ do- result <- try @SomeException $ do- S.fork $ do+ defaultMain+ $ testGroup "All"+ $ [ testCase "Failing in finalizer doesn't break everything" $ do+ finalizer1CalledVar <- newTVarIO False+ finalizer2CalledVar <- newTVarIO False+ result <-+ let finalizer1 =+ atomically $ writeTVar finalizer1CalledVar True+ finalizer2 =+ do+ atomically $ writeTVar finalizer2CalledVar True+ throwIO (userError "finalizer2 failed")+ in try @SomeException $ do+ S.forkFinally finalizer1 $ do+ S.forkFinally finalizer2 $ threadDelay 100+ threadDelay (10 ^ 4)+ finalizer2Called <- atomically (readTVar finalizer2CalledVar)+ finalizer1Called <- atomically (readTVar finalizer1CalledVar)+ assertEqual "finalizer2 not called" True finalizer2Called+ assertEqual "finalizer1 not called" True finalizer1Called+ assertEqual "result is left" True (isLeft result)+ assertEqual "Invalid result" True ("finalizer2 failed" `isInfixOf` show result),+ testCase "Forked threads run fine" $ do+ replicateM_ 100000 $ do+ var <- newMVar 0+ let increment = modifyMVar_ var (return . succ)+ semaphore <- SSem.new 0 S.fork $ do- error "!"- threadDelay $ 10^6- threadDelay $ 10^6- assertBool "" (isLeft result)- ,- testCase "Slaves are finalized before master" $ do- replicateM_ 100000 $ do- ready <- newEmptyMVar- var <- newEmptyTMVarIO- thread <-- S.forkFinally (atomically (tryPutTMVar var 1)) $ do- S.forkFinally (atomically (tryPutTMVar var 0)) $- threadDelay $ 10^6- putMVar ready ()- threadDelay $ 10^6- takeMVar ready- killThread thread- assertEqual "First finalizer is not slave" 0 =<< atomically (readTMVar var)- ,- testCase "Slave thread finalizer is not interrupted by its own death (#11)" $ do- -- Set up the ref that should be written to by thread 2's finalizer,- -- otherwise there's a bug.- ref <- newIORef True-- -- Let the main thread know when it should check the above IORef.- done <- newEmptyMVar-- -- The gist of the test below: assert that, when a thread fails (here, the- -- inner thread), its finalizer is not interrupted by a ThreadKilled- -- thrown by the parent, which was originally triggered by its own death.-- S.forkFinally (putMVar done ()) $ do+ increment+ semaphore' <- SSem.new (-1)+ S.fork $ do+ increment+ SSem.signal semaphore'+ S.fork $ do+ increment+ SSem.signal semaphore'+ SSem.wait semaphore'+ SSem.signal semaphore+ SSem.wait semaphore+ assertEqual "" 3 =<< readMVar var,+ testCase "Killing a thread kills deep slaves" $ do+ replicateM_ 100000 $ do+ var <- newMVar 0+ semaphore <- SSem.new 0+ thread <-+ S.forkFinally (SSem.signal semaphore) $ do+ join $ forkWait $ do+ join $ forkWait $ do+ w <- forkWait $ do+ threadDelay $ 10 ^ 6+ modifyMVar_ var (return . succ)+ threadDelay $ 10 ^ 6+ modifyMVar_ var (return . succ)+ w+ killThread thread+ SSem.wait semaphore+ assertEqual "" 0 =<< readMVar var,+ testCase "Dying normally kills slaves" $ do+ replicateM_ 100000 $ do+ var <- newIORef 0+ let increment = modifyIORef var (+ 1)+ semaphore <- SSem.new 0+ S.forkFinally (SSem.signal semaphore) $ do+ S.fork $ do+ threadDelay $ 10 ^ 6+ increment+ S.fork $ do+ threadDelay $ 10 ^ 6+ increment+ SSem.wait semaphore+ assertEqual "" 0 =<< readIORef var,+ testCase "Finalization is in order" $ do+ replicateM_ 100000 $ do+ var <- newMVar []+ semaphore <- SSem.new 0+ S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (1 :)) >> SSem.signal semaphore)) $ do+ semaphore' <- SSem.new 0+ S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (2 :)) >> SSem.signal semaphore')) $ do+ S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3 :)))) $ return ()+ S.forkFinally (uninterruptibleMask_ (modifyMVar_ var (return . (3 :)))) $ return ()+ SSem.wait semaphore'+ SSem.wait semaphore+ assertEqual "" [1, 2, 3, 3] =<< readMVar var,+ testCase "Exceptions don't get lost" $ do+ replicateM_ 100000 $ do+ result <- try @SomeException $ do+ S.fork $ do+ S.fork $ do+ error "!"+ threadDelay $ 10 ^ 6+ threadDelay $ 10 ^ 6+ assertBool "" (isLeft result),+ testCase "Slaves are finalized before master" $ do+ replicateM_ 100000 $ do+ ready <- newEmptyMVar+ var <- newEmptyTMVarIO+ thread <-+ S.forkFinally (atomically (tryPutTMVar var 1)) $ do+ S.forkFinally (atomically (tryPutTMVar var 0))+ $ threadDelay+ $ 10+ ^ 6+ putMVar ready ()+ threadDelay $ 10 ^ 6+ takeMVar ready+ killThread thread+ assertEqual "First finalizer is not slave" 0 =<< atomically (readTMVar var),+ testCase "Slave thread finalizer is not interrupted by its own death (#11)" $ do+ -- Set up the ref that should be written to by thread 2's finalizer,+ -- otherwise there's a bug.+ ref <- newIORef True - -- Let thread 2 know when it should die, with thread 1's exception- -- handler in place.- ready <- newEmptyMVar+ -- Let the main thread know when it should check the above IORef.+ done <- newEmptyMVar - S.forkFinally- (catch @SomeException (threadDelay (10^5)) (\_ -> writeIORef ref False)) $ do+ -- The gist of the test below: assert that, when a thread fails (here, the+ -- inner thread), its finalizer is not interrupted by a ThreadKilled+ -- thrown by the parent, which was originally triggered by its own death. - -- Wait until thread 1 is ready for us to die.- takeMVar ready+ S.forkFinally (putMVar done ()) $ do+ -- Let thread 2 know when it should die, with thread 1's exception+ -- handler in place.+ ready <- newEmptyMVar - -- Die.- throwIO (userError "")+ S.forkFinally+ (catch @SomeException (threadDelay (10 ^ 5)) (\_ -> writeIORef ref False))+ $ do+ -- Wait until thread 1 is ready for us to die.+ takeMVar ready - catch @SomeException- ( -- Tell thread 2 we're ready for it to die- putMVar ready () >>+ -- Die.+ throwIO (userError "") - -- Sleep until thread 2 kills us.- threadDelay (10^5*2)- )- -- Ignore thread 2's exception, so we don't propagate it up to the- -- main thread.- (\ _ -> return ())+ catch @SomeException+ ( -- Tell thread 2 we're ready for it to die+ putMVar ready ()+ >>+ -- Sleep until thread 2 kills us.+ threadDelay (10 ^ 5 * 2)+ )+ -- Ignore thread 2's exception, so we don't propagate it up to the+ -- main thread.+ (\_ -> return ()) - takeMVar done- assertBool "Slave thread finalizer interrupted" =<< readIORef ref- ,- testCase "Master kills all slaves, even if it is thrown an exception during (#13)" $ do- survived <- newEmptyTMVarIO- ready <- newEmptyMVar- done <- newEmptyMVar- thread <-- S.fork $ do- S.forkFinally (atomically (tryPutTMVar survived True)) $ do- uninterruptibleMask_ (putMVar ready () >> threadDelay (10^6))- atomically (putTMVar survived False)- takeMVar ready- putMVar done ()- takeMVar done- threadDelay $ 10^5 -- be reasonably sure it's trying to kill its child- killThread thread- assertBool "Slave thread not killed by master" =<< atomically (takeTMVar survived)- ]+ takeMVar done+ assertBool "Slave thread finalizer interrupted" =<< readIORef ref,+ testCase "Master kills all slaves, even if it is thrown an exception during (#13)" $ do+ survived <- newEmptyTMVarIO+ ready <- newEmptyMVar+ done <- newEmptyMVar+ thread <-+ S.fork $ do+ S.forkFinally (atomically (tryPutTMVar survived True)) $ do+ uninterruptibleMask_ (putMVar ready () >> threadDelay (10 ^ 6))+ atomically (putTMVar survived False)+ takeMVar ready+ putMVar done ()+ takeMVar done+ threadDelay $ 10 ^ 5 -- be reasonably sure it's trying to kill its child+ killThread thread+ assertBool "Slave thread not killed by master" =<< atomically (takeTMVar survived)+ ] forkWait :: IO a -> IO (IO ()) forkWait io =