on master help sign in
Use own error type for unlifting errors (#34)

* Use own error type for unlifting errors * Effectful.Internal.Error.EffectfulError -> Effectful.Internal.Unlift.UnliftError * Moved more unlifting related code to Effectful.Internal.Unlift * Apply suggestions from code review Co-authored-by: Andrzej Rybczak <arybczak@users.noreply.github.com> * Added Generic instances for unlifting types * Strict fields in UnliftError * Naming of tests * Use Optics in test suite * Added `unsafeUnliftToEff` * Fixed link in description of effectful-core * Added note for descriptive error messages to UnliftError * UnliftTests: Follow naming scheme in other test modules Co-authored-by: Andrzej Rybczak <arybczak@users.noreply.github.com>

commit1a4a824f4b8a0d01e7c1d4c9a486804144bb3919 authorMann mit Hut <mmhat@users.noreply.github.com> dateThu 11 Nov 2021, 10:40 (5 years ago) parent40abe51
9 files changed+317−116
effectful-core/effectful-core.cabal+2−3 effectful-core/src/Effectful/Internal/Monad.hs+47−101 effectful-core/src/Effectful/Internal/Unlift.hs+160−12 effectful-core/src/Effectful/Monad.hs+1 effectful/effectful.cabal+3 effectful/src/Effectful.hs+1 effectful/tests/Main.hs+2 effectful/tests/StateTests.hs+1 effectful/tests/UnliftTests.hs+100
effectful-core/effectful-core.cabal+2−3
@@ -13,9 +13,8 @@ description: An easy to use, performant extensible effects library with seamless
1313 integration with the existing Haskell ecosystem.
1414 .
1515 This library provides core definitions with a minimal dependency
16− footprint. See the @<https://hackage.haskell.org/package/effectful
17− effectful>@ package for documentation and the "batteries-included"
18− variant.
16+ footprint. See the @<https://hackage.haskell.org/package/effectful effectful>@
17+ package for documentation and the "batteries-included" variant.
1918
2019extra-source-files: CHANGELOG.md
2120
effectful-core/src/Effectful/Internal/Monad.hs+47−101
@@ -33,9 +33,11 @@ module Effectful.Internal.Monad
3333 , unliftStrategy
3434 , withUnliftStrategy
3535 , withEffToIO
36+ , UnliftError(..)
3637
3738 --- *** Low-level helpers
3839 , unsafeLiftMapIO
40+ , unsafeUnliftToEff
3941 , seqUnliftEff
4042 , concUnliftEff
4143
@@ -66,7 +68,6 @@ module Effectful.Internal.Monad
6668 ) where
6769
6870import Control.Applicative (liftA2)
69−import Control.Concurrent (myThreadId)
7071import Control.Monad.Base
7172import Control.Monad.Fix
7273import Control.Monad.IO.Class
@@ -83,7 +84,6 @@ import qualified Control.Monad.Catch as C
8384import Effectful.Internal.Effect
8485import Effectful.Internal.Env
8586import Effectful.Internal.Unlift
86−import Effectful.Internal.Utils
8787
8888type role Eff nominal representational
8989
@@ -166,21 +166,30 @@ unsafeEff_ m = unsafeEff $ \_ -> m
166166unsafeLiftMapIO :: (IO a -> IO b) -> Eff es a -> Eff es b
167167unsafeLiftMapIO f m = unsafeEff $ \es -> f (unEff m es)
168168
169+----------------------------------------
170+-- Unlifting Eff
171+
172+-- | Get the current 'UnliftStrategy'.
173+unliftStrategy :: IOE :> es => Eff es UnliftStrategy
174+unliftStrategy = do
175+ IdE (IOE unlift) <- getEffect
176+ pure unlift
177+
178+-- | Locally override the 'UnliftStrategy' with the given value.
179+withUnliftStrategy :: IOE :> es => UnliftStrategy -> Eff es a -> Eff es a
180+withUnliftStrategy unlift = localEffect $ \_ -> IdE (IOE unlift)
181+
169182-- | Lower 'Eff' operations into 'IO' ('SeqUnlift').
183+--
184+-- Exceptions thrown by this function:
185+--
186+-- - 'InvalidUseOfSeqUnlift' if the unlift function is used in another thread.
170187seqUnliftEff
171188 :: HasCallStack
172189 => Env es
173190 -> ((forall r. Eff es r -> IO r) -> IO a)
174191 -> IO a
175−seqUnliftEff es k = do
176− tid0 <- myThreadId
177− k $ \m -> do
178− tid <- myThreadId
179− if tid `eqThreadId` tid0
180− then unEff m es
181− else error
182− $ "If you want to use the unlifting function to run Eff operations "
183− ++ "in multiple threads, have a look at UnliftStrategy (ConcUnlift)."
192+seqUnliftEff es k = seqUnliftIO k es unEff
184193
185194-- | Lower 'Eff' operations into 'IO' ('ConcUnlift').
186195concUnliftEff
@@ -190,14 +199,33 @@ concUnliftEff
190199 -> Limit
191200 -> ((forall r. Eff es r -> IO r) -> IO a)
192201 -> IO a
193−concUnliftEff es Ephemeral (Limited uses) k =
194− ephemeralConcUnliftIO uses k es unEff
195−concUnliftEff es Ephemeral Unlimited k =
196− ephemeralConcUnliftIO maxBound k es unEff
197−concUnliftEff es Persistent (Limited threads) k =
198− persistentConcUnliftIO False threads k es unEff
199−concUnliftEff es Persistent Unlimited k =
200− persistentConcUnliftIO True maxBound k es unEff
202+concUnliftEff es persistence limit k = concUnliftIO persistence limit k es unEff
203+
204+-- | Create an unlifting function with the current 'UnliftStrategy'.
205+--
206+-- This function is equivalent to 'withRunInIO', but has a 'HasCallStack'
207+-- constraint for accurate stack traces in case an insufficiently powerful
208+-- 'UnliftStrategy' is used and the unlifting function fails.
209+--
210+-- /Note:/ the strategy is reset to 'SeqUnlift' for new threads.
211+withEffToIO
212+ :: (HasCallStack, IOE :> es)
213+ => ((forall r. Eff es r -> IO r) -> IO a)
214+ -- ^ Continuation with the unlifting function in scope.
215+ -> Eff es a
216+withEffToIO f = unliftStrategy >>= \case
217+ SeqUnlift -> unsafeEff $ \es -> seqUnliftEff es f
218+ ConcUnlift p b -> withUnliftStrategy SeqUnlift $ do
219+ unsafeEff $ \es -> concUnliftEff es p b f
220+
221+-- | An unlifing function that lifts a function and its callback from 'IO' to
222+-- 'Eff'.
223+--
224+-- This function is unsafe since there are no checks wrt. to concurrency at all.
225+-- Use with care.
226+unsafeUnliftToEff :: ((a -> IO b) -> IO c) -> (a -> Eff es b) -> Eff es c
227+unsafeUnliftToEff k f = unsafeEff $ \es -> do
228+ k ((`unEff` es) . f)
201229
202230----------------------------------------
203231-- Base
@@ -310,88 +338,6 @@ instance Prim :> es => PrimMonad (Eff es) where
310338 type PrimState (Eff es) = RealWorld
311339 primitive = unsafeEff_ . IO
312340
313−----------------------------------------
314−-- Unlift strategies
315−
316−-- | The strategy to use when unlifting 'Eff' operations via 'withRunInIO' or
317−-- the 'Effectful.Interpreter.localUnlift' family.
318−data UnliftStrategy
319− = SeqUnlift
320− -- ^ The fastest strategy and a default setting for 'IOE'. An attempt to call
321− -- the unlifting function in threads distinct from its creator will result in
322− -- a runtime error.
323− | ConcUnlift !Persistence !Limit
324− -- ^ A strategy that makes it possible for the unlifting function to be called
325− -- in threads distinct from its creator. See 'Persistence' and 'Limit'
326− -- settings for more information.
327− deriving (Eq, Ord, Show)
328−
329−-- | Persistence setting for the 'ConcUnlift' strategy.
330−--
331−-- Different functions require different persistence strategies. Examples:
332−--
333−-- - Lifting 'pooledMapConcurrentlyN' from the @unliftio@ library requires the
334−-- 'Ephemeral' strategy as we don't want jobs to share environment changes
335−-- made by previous jobs run in the same worker thread.
336−--
337−-- - Lifting 'Control.Concurrent.forkIOWithUnmask' requires the 'Persistent'
338−-- strategy, otherwise the unmasking function would start with a fresh
339−-- environment each time it's called.
340−data Persistence
341− = Ephemeral
342− -- ^ Don't persist the environment between calls to the unlifting function in
343− -- threads distinct from its creator.
344− | Persistent
345− -- ^ Persist the environment between calls to the unlifting function within a
346− -- particular thread.
347− deriving (Eq, Ord, Show)
348−
349−-- | Limit setting for the 'ConcUnlift' strategy.
350−data Limit
351− = Limited !Int
352− -- ^ Behavior dependent on the 'Persistence' setting.
353− --
354− -- For 'Ephemeral', it limits the amount of uses of the unlifting function in
355− -- threads distinct from its creator to @N@. The unlifting function will
356− -- create @N@ copies of the environment when called @N@ times and @K+1@ copies
357− -- when called @K < N@ times.
358− --
359− -- For 'Persistent', it limits the amount of threads, distinct from the
360− -- creator of the unlifting function, it can be called in to @N@. The amount
361− -- of calls to the unlifting function within a particular threads is
362− -- unlimited. The unlifting function will create @N@ copies of the environment
363− -- when called in @N@ threads and @K+1@ copies when called in @K < N@ threads.
364− | Unlimited
365− -- ^ Unlimited use of the unlifting function.
366− deriving (Eq, Ord, Show)
367−
368−-- | Get the current 'UnliftStrategy'.
369−unliftStrategy :: IOE :> es => Eff es UnliftStrategy
370−unliftStrategy = do
371− IdE (IOE unlift) <- getEffect
372− pure unlift
373−
374−-- | Locally override the 'UnliftStrategy' with the given value.
375−--
376−-- /Note:/ the strategy is reset to 'SeqUnlift' for new threads.
377−withUnliftStrategy :: IOE :> es => UnliftStrategy -> Eff es a -> Eff es a
378−withUnliftStrategy unlift = localEffect $ \_ -> IdE (IOE unlift)
379−
380−-- | Create an unlifting function with the current 'UnliftStrategy'.
381−--
382−-- This function is equivalent to 'withRunInIO', but has a 'HasCallStack'
383−-- constraint for accurate stack traces in case an insufficiently powerful
384−-- 'UnliftStrategy' is used and the unlifting function fails.
385−withEffToIO
386− :: (HasCallStack, IOE :> es)
387− => ((forall r. Eff es r -> IO r) -> IO a)
388− -- ^ Continuation with the unlifting function in scope.
389− -> Eff es a
390−withEffToIO f = unliftStrategy >>= \case
391− SeqUnlift -> unsafeEff $ \es -> seqUnliftEff es f
392− ConcUnlift p b -> withUnliftStrategy SeqUnlift $ do
393− unsafeEff $ \es -> concUnliftEff es p b f
394−
395341----------------------------------------
396342-- Handler
397343
effectful-core/src/Effectful/Internal/Unlift.hs+160−12
@@ -1,19 +1,33 @@
1+{-# LANGUAGE DeriveGeneric #-}
12{-# LANGUAGE MagicHash #-}
23{-# LANGUAGE UnboxedTuples #-}
34{-# OPTIONS_HADDOCK not-home #-}
4−-- | Implementation of concurrent unlifts.
5+-- | Implementation of sequential and concurrent unlifts.
56--
67-- This module is intended for internal use only, and may change without warning
78-- in subsequent releases.
89module Effectful.Internal.Unlift
9− ( ephemeralConcUnliftIO
10+ ( -- * Unlifting strategies
11+ UnliftStrategy(..)
12+ , Persistence(..)
13+ , Limit(..)
14+
15+ -- * Unlifting functions
16+ , seqUnliftIO
17+ , concUnliftIO
18+ , ephemeralConcUnliftIO
1019 , persistentConcUnliftIO
20+
21+ -- * Unlifting errors
22+ , UnliftError(..)
1123 ) where
1224
1325import Control.Concurrent
26+import Control.Exception
1427import Control.Monad
1528import GHC.Conc.Sync (ThreadId(..))
1629import GHC.Exts (mkWeak#, mkWeakNoFinalizer#)
30+import GHC.Generics (Generic)
1731import GHC.IO (IO(..))
1832import GHC.Stack (HasCallStack)
1933import GHC.Weak (Weak(..))
@@ -23,18 +37,119 @@ import qualified Data.IntMap.Strict as IM
2337import Effectful.Internal.Env
2438import Effectful.Internal.Utils
2539
40+----------------------------------------
41+-- Unlift strategies
42+
43+-- | The strategy to use when unlifting 'Eff' operations via 'withRunInIO' or
44+-- the 'Effectful.Interpreter.localUnlift' family.
45+data UnliftStrategy
46+ = SeqUnlift
47+ -- ^ The fastest strategy and a default setting for 'IOE'. An attempt to call
48+ -- the unlifting function in threads distinct from its creator will result in
49+ -- a runtime error.
50+ | ConcUnlift !Persistence !Limit
51+ -- ^ A strategy that makes it possible for the unlifting function to be called
52+ -- in threads distinct from its creator. See 'Persistence' and 'Limit'
53+ -- settings for more information.
54+ deriving (Eq, Generic, Ord, Show)
55+
56+-- | Persistence setting for the 'ConcUnlift' strategy.
57+--
58+-- Different functions require different persistence strategies. Examples:
59+--
60+-- - Lifting 'pooledMapConcurrentlyN' from the @unliftio@ library requires the
61+-- 'Ephemeral' strategy as we don't want jobs to share environment changes
62+-- made by previous jobs run in the same worker thread.
63+--
64+-- - Lifting 'Control.Concurrent.forkIOWithUnmask' requires the 'Persistent'
65+-- strategy, otherwise the unmasking function would start with a fresh
66+-- environment each time it's called.
67+data Persistence
68+ = Ephemeral
69+ -- ^ Don't persist the environment between calls to the unlifting function in
70+ -- threads distinct from its creator.
71+ | Persistent
72+ -- ^ Persist the environment between calls to the unlifting function within a
73+ -- particular thread.
74+ deriving (Eq, Generic, Ord, Show)
75+
76+-- | Limit setting for the 'ConcUnlift' strategy.
77+data Limit
78+ = Limited !Int
79+ -- ^ Behavior dependent on the 'Persistence' setting.
80+ --
81+ -- For 'Ephemeral', it limits the amount of uses of the unlifting function in
82+ -- threads distinct from its creator to @N@. The unlifting function will
83+ -- create @N@ copies of the environment when called @N@ times and @K+1@ copies
84+ -- when called @K < N@ times.
85+ --
86+ -- For 'Persistent', it limits the amount of threads, distinct from the
87+ -- creator of the unlifting function, it can be called in to @N@. The amount
88+ -- of calls to the unlifting function within a particular threads is
89+ -- unlimited. The unlifting function will create @N@ copies of the environment
90+ -- when called in @N@ threads and @K+1@ copies when called in @K < N@ threads.
91+ | Unlimited
92+ -- ^ Unlimited use of the unlifting function.
93+ deriving (Eq, Generic, Ord, Show)
94+
95+----------------------------------------
96+-- Unlift functions
97+
98+-- | Sequential unlift.
99+--
100+-- Exceptions thrown by this function:
101+--
102+-- - 'InvalidUseOfSeqUnlift' if the unlift function is used in another thread.
103+seqUnliftIO
104+ :: HasCallStack
105+ => ((forall r. m r -> IO r) -> IO a)
106+ -> Env es
107+ -> (forall r. m r -> Env es -> IO r)
108+ -> IO a
109+seqUnliftIO k es unEff = do
110+ tid0 <- myThreadId
111+ k $ \m -> do
112+ tid <- myThreadId
113+ if tid `eqThreadId` tid0
114+ then unEff m es
115+ else throwIO InvalidUseOfSeqUnlift
116+
117+-- | Concurrent unlift for various strategies and limits.
118+concUnliftIO
119+ :: HasCallStack
120+ => Persistence
121+ -> Limit
122+ -> ((forall r. m r -> IO r) -> IO a)
123+ -> Env es
124+ -> (forall r. m r -> Env es -> IO r)
125+ -> IO a
126+concUnliftIO Ephemeral (Limited uses) k =
127+ ephemeralConcUnliftIO uses k
128+concUnliftIO Ephemeral Unlimited k =
129+ ephemeralConcUnliftIO maxBound k
130+concUnliftIO Persistent (Limited threads) k =
131+ persistentConcUnliftIO False threads k
132+concUnliftIO Persistent Unlimited k =
133+ persistentConcUnliftIO True maxBound k
134+
26135-- | Concurrent unlift that doesn't preserve the environment between calls to
27136-- the unlifting function in threads other than its creator.
137+--
138+-- Exceptions thrown by this function:
139+--
140+-- - 'InvalidNumberOfUses' if the number of uses is less or equal zero.
141+-- - 'ExceededNumberOfUses' if the unlift function is called too often.
28142ephemeralConcUnliftIO
29143 :: HasCallStack
30144 => Int
145+ -- ^ Number of permitted uses of the unlift function.
31146 -> ((forall r. m r -> IO r) -> IO a)
32147 -> Env es
33148 -> (forall r. m r -> Env es -> IO r)
34149 -> IO a
35150ephemeralConcUnliftIO uses k es0 unEff = do
36151 unless (uses > 0) $ do
37− error $ "Invalid number of uses: " ++ show uses
152+ throwIO $ InvalidNumberOfUses uses
38153 tid0 <- myThreadId
39154 -- Create a copy of the environment as a template for the other threads to
40155 -- use. This can't be done from inside the callback as the environment might
@@ -45,10 +160,7 @@ ephemeralConcUnliftIO uses k es0 unEff = do
45160 es <- myThreadId >>= \case
46161 tid | tid0 `eqThreadId` tid -> pure es0
47162 _ -> modifyMVar mvUses $ \case
48− 0 -> error
49− $ "Number of permitted calls (" ++ show uses ++ ") to the unlifting "
50− ++ "function in other threads was exceeded. Please increase the limit "
51− ++ "or use the unlimited variant."
163+ 0 -> throwIO $ ExceededNumberOfUses uses
52164 1 -> pure (0, esTemplate)
53165 n -> do
54166 let newUses = n - 1
@@ -58,17 +170,24 @@ ephemeralConcUnliftIO uses k es0 unEff = do
58170
59171-- | Concurrent unlift that preserves the environment between calls to the
60172-- unlifting function within a particular thread.
173+--
174+-- Exceptions thrown by this function:
175+--
176+-- - 'InvalidNumberOfThreads' if the number of threads is less or equal zero.
177+-- - 'ExceededNumberOfThreads' if the unlift function is called by too many
178+-- threads.
61179persistentConcUnliftIO
62180 :: HasCallStack
63181 => Bool
64182 -> Int
183+ -- ^ Number of threads that are allowed to use the unlift function.
65184 -> ((forall r. m r -> IO r) -> IO a)
66185 -> Env es
67186 -> (forall r. m r -> Env es -> IO r)
68187 -> IO a
69188persistentConcUnliftIO cleanUp threads k es0 unEff = do
70189 unless (threads > 0) $ do
71− error $ "Invalid number of threads: " ++ show threads
190+ throwIO $ InvalidNumberOfThreads threads
72191 tid0 <- myThreadId
73192 -- Create a copy of the environment as a template for the other threads to
74193 -- use. This can't be done from inside the callback as the environment might
@@ -86,10 +205,7 @@ persistentConcUnliftIO cleanUp threads k es0 unEff = do
86205 case mes of
87206 Just es -> pure (te, es)
88207 Nothing -> case teCapacity te of
89− 0 -> error
90− $ "Number of other threads (" ++ show threads ++ ") permitted to "
91− ++ "use the unlifting function was exceeded. Please increase the "
92− ++ "limit or use the unlimited variant."
208+ 0 -> throwIO $ ExceededNumberOfThreads threads
93209 1 -> do
94210 wkTidEs <- mkWeakThreadIdEnv tid esTemplate wkTid i mvEntries cleanUp
95211 let newEntries = ThreadEntries
@@ -107,6 +223,38 @@ persistentConcUnliftIO cleanUp threads k es0 unEff = do
107223 newEntries `seq` pure (newEntries, es)
108224 unEff m es
109225
226+----------------------------------------
227+-- Errors
228+
229+-- | An type used for errors occuring in unlifting functions.
230+--
231+-- Use the 'displayException' method of the 'Exception' type class to get
232+-- descriptive error messages.
233+data UnliftError
234+ = InvalidNumberOfThreads !Int
235+ | InvalidNumberOfUses !Int
236+ | InvalidUseOfSeqUnlift
237+ | ExceededNumberOfThreads !Int
238+ | ExceededNumberOfUses !Int
239+ deriving (Eq, Generic, Show)
240+
241+instance Exception UnliftError where
242+ displayException (InvalidNumberOfThreads threads) =
243+ "Invalid number of threads: " ++ show threads
244+ displayException (InvalidNumberOfUses uses) =
245+ "Invalid number of uses: " ++ show uses
246+ displayException (ExceededNumberOfThreads threads) =
247+ "Number of other threads (" ++ show threads ++ ") permitted to use the " ++
248+ "unlifting function was exceeded. Please increase the limit or use the " ++
249+ "unlimited variant."
250+ displayException (ExceededNumberOfUses uses) =
251+ "Number of permitted calls (" ++ show uses ++ ") to the unlifting " ++
252+ "function in other threads was exceeded. Please increase the limit or " ++
253+ "use the unlimited variant."
254+ displayException InvalidUseOfSeqUnlift =
255+ "If you want to use the unlifting function to run Eff operations in " ++
256+ "multiple threads, have a look at UnliftStrategy (ConcUnlift)."
257+
110258----------------------------------------
111259-- Data types
112260
effectful-core/src/Effectful/Monad.hs+1
@@ -18,6 +18,7 @@ module Effectful.Monad
1818 , unliftStrategy
1919 , withUnliftStrategy
2020 , withEffToIO
21+ , UnliftError(..)
2122
2223 -- * Re-exports
2324 , MonadIO(..)
effectful/effectful.cabal+3
@@ -102,8 +102,10 @@ test-suite test
102102 build-depends: base
103103 , containers
104104 , effectful
105+ , effectful-core
105106 , exceptions
106107 , lifted-base
108+ , optics-core >= 0.4
107109 , tasty
108110 , tasty-hunit
109111 , unliftio
@@ -119,6 +121,7 @@ test-suite test
119121 ErrorTests
120122 StateTests
121123 TimeoutTests
124+ UnliftTests
122125 Utils
123126 -- These are examples; We do not actually run them but list
124127 -- them here in order to check if they compile.
effectful/src/Effectful.hs+1
@@ -22,6 +22,7 @@ module Effectful
2222 , unliftStrategy
2323 , withUnliftStrategy
2424 , withEffToIO
25+ , UnliftError(..)
2526
2627 -- * 'Effect' handlers
2728
effectful/tests/Main.hs+2
@@ -8,6 +8,7 @@ import EnvironmentTests
88import ErrorTests
99import StateTests
1010import TimeoutTests
11+import UnliftTests
1112
1213main :: IO ()
1314main = defaultMain $ testGroup "effectful"
@@ -17,4 +18,5 @@ main = defaultMain $ testGroup "effectful"
1718 , errorTests
1819 , stateTests
1920 , timeoutTests
21+ , unliftTests
2022 ]
effectful/tests/StateTests.hs+1
@@ -97,6 +97,7 @@ test_localEffects = runEff $ do
9797 where
9898 expected :: Int
9999 expected = 4
100+
100101----------------------------------------
101102-- Helpers
102103
effectful/tests/UnliftTests.hsnew file+100
@@ -0,0 +1,100 @@
1+{-# LANGUAGE OverloadedLabels #-}
2+module UnliftTests (unliftTests) where
3+
4+import Optics.Core
5+import Test.Tasty
6+import Test.Tasty.HUnit
7+import qualified UnliftIO.Async as A
8+
9+import Effectful
10+import qualified Utils as U
11+
12+unliftTests :: TestTree
13+unliftTests = testGroup "Unlift"
14+ [ testCase "Reset strategy in new thread" test_resetStrategy
15+ , testCase "SeqUnlift in new thread" test_seqUnliftInNewThread
16+ , testGroup "Ephemeral strategy"
17+ [ testCase "Invalid limit" test_ephemeralInvalid
18+ , testCase "Uses in same thread" test_ephemeralSameThread
19+ , testCase "Uses in multiple threads" test_ephemeralMultipleThreads
20+ ]
21+ , testGroup "Persistent strategy"
22+ [ testCase "Invalid limit" test_persistentInvalid
23+ , testCase "Uses in same thread" test_persistentSameThread
24+ , testCase "Uses in multiple threads" test_persistentMultipleThreads
25+ ]
26+ ]
27+
28+test_resetStrategy :: Assertion
29+test_resetStrategy = runEff $ do
30+ s <- withUnliftStrategy (ConcUnlift Ephemeral Unlimited) $ do
31+ withEffToIO $ \runInIO -> do
32+ inThread $ runInIO unliftStrategy
33+ U.assertEqual "correct strategy" SeqUnlift s
34+
35+test_seqUnliftInNewThread :: Assertion
36+test_seqUnliftInNewThread = runEff $ do
37+ assertThrowsUnliftError "InvalidUseOfSeqUnlift error"
38+ (has #_InvalidUseOfSeqUnlift) $ do
39+ withUnliftStrategy SeqUnlift $ do
40+ withEffToIO $ \runInIO -> do
41+ inThread $ runInIO $ return ()
42+
43+test_ephemeralInvalid :: Assertion
44+test_ephemeralInvalid = runEff $ do
45+ assertThrowsUnliftError "InvalidNumberOfUses error"
46+ (has #_InvalidNumberOfUses) $ do
47+ withUnliftStrategy (ConcUnlift Ephemeral $ Limited 0) $ do
48+ withEffToIO $ \_ -> return ()
49+
50+test_ephemeralSameThread :: Assertion
51+test_ephemeralSameThread = runEff $ do
52+ assertThrowsUnliftError "ExceededNumberOfUses error"
53+ (has #_ExceededNumberOfUses) $ do
54+ withUnliftStrategy (ConcUnlift Ephemeral $ Limited 1) $ do
55+ withEffToIO $ \runInIO -> inThread $ do
56+ runInIO $ return ()
57+ runInIO $ return ()
58+
59+test_ephemeralMultipleThreads :: Assertion
60+test_ephemeralMultipleThreads = runEff $ do
61+ assertThrowsUnliftError "ExceededNumberOfUses error"
62+ (has #_ExceededNumberOfUses) $ do
63+ withUnliftStrategy (ConcUnlift Ephemeral $ Limited 1) $ do
64+ withEffToIO $ \runInIO -> do
65+ inThread $ runInIO $ return ()
66+ inThread $ runInIO $ return ()
67+
68+test_persistentInvalid :: Assertion
69+test_persistentInvalid = runEff $ do
70+ assertThrowsUnliftError "InvalidNumberOfThreads error"
71+ (has #_InvalidNumberOfThreads) $ do
72+ withUnliftStrategy (ConcUnlift Persistent $ Limited 0) $ do
73+ withEffToIO $ \_ -> return ()
74+
75+test_persistentSameThread :: Assertion
76+test_persistentSameThread = runEff $ do
77+ withUnliftStrategy (ConcUnlift Persistent $ Limited 1) $ do
78+ withEffToIO $ \runInIO -> inThread $ do
79+ runInIO $ return ()
80+ runInIO $ return ()
81+
82+test_persistentMultipleThreads :: Assertion
83+test_persistentMultipleThreads = runEff $ do
84+ assertThrowsUnliftError "ExceededNumberOfThreads error"
85+ (has #_ExceededNumberOfThreads) $ do
86+ withUnliftStrategy (ConcUnlift Persistent $ Limited 1) $ do
87+ withEffToIO $ \runInIO -> do
88+ inThread $ runInIO $ return ()
89+ inThread $ runInIO $ return ()
90+
91+----------------------------------------
92+-- Helpers
93+
94+assertThrowsUnliftError
95+ :: IOE :> es
96+ => String -> (UnliftError -> Bool) -> Eff es a -> Eff es ()
97+assertThrowsUnliftError = U.assertThrows
98+
99+inThread :: IO a -> IO a
100+inThread k = A.async k >>= A.wait