on master help sign in
Pass on exceptions and results from child to parent
commitc49bcc74061c7db459262c0e28e881de05cd8032 authorHarendra Kumar <harendra.kumar@gmail.com> dateWed 21 Jun 2017, 15:15 (9 years ago) parent6fd6651
4 files changed+195−83
duct.cabal+1 src/Duct/AsyncT.hs+49−37 src/Duct/Context.hs+13−9 src/Duct/Threads.hs+132−37
duct.cabal+1
@@ -36,6 +36,7 @@ library
3636 , ghc-prim
3737
3838 -- libraries not bundled with GHC
39+ , exceptions
3940 , lifted-base
4041 , monad-control
4142 , mtl
src/Duct/AsyncT.hs+49−37
@@ -14,17 +14,17 @@ module Duct.AsyncT
1414 , waitAsync
1515 , (<**)
1616 , onNothing
17− , waitForChildren
1817 , dbg
1918 )
2019where
2120
2221import Control.Applicative (Alternative (..))
23−import Control.Concurrent (ThreadId)
22+import Control.Concurrent (ThreadId, killThread)
2423import Control.Concurrent.STM (TChan, atomically, newTChan,
2524 readTChan)
26−import Control.Exception (SomeException, catch)
2725import Control.Monad.Base (MonadBase (..), liftBaseDefault)
26+import Control.Monad.Catch (MonadCatch, MonadThrow, try,
27+ throwM, SomeException)
2828import Control.Monad.State (MonadIO (..), MonadPlus (..),
2929 MonadState (..), StateT (..),
3030 liftM, modify, runStateT, when)
@@ -157,50 +157,62 @@ instance (MonadBaseControl b m, MonadIO m) => MonadBaseControl b (AsyncT m) wher
157157-- Thread management
158158------------------------------------------------------------------------------
159159
160−drainChildren :: MonadIO m => TChan ThreadId -> [ThreadId] -> m ()
161−drainChildren chan pending =
162− if length pending == 0
163− then return ()
160+drainChildren :: (MonadIO m, MonadThrow m)
161+ => TChan (ChildEvent a) -> [ThreadId] -> [a] -> m [a]
162+drainChildren chan pending res =
163+ if pending == []
164+ then return res
164165 else do
165− tid <- liftIO $ atomically $ readTChan chan
166− dbg $ "Reaping child: " ++ show tid
167− drainChildren chan $ delete tid pending
168−
169−waitForChildren :: MonadIO m => TChan ThreadId -> IORef [ThreadId] -> m ()
166+ ev <- liftIO $ atomically $ readTChan chan
167+ case ev of
168+ ChildDone tid er -> do
169+ dbg $ "Reaping child: " ++ show tid
170+ case er of
171+ Left e -> throwM e
172+ Right r -> do
173+ let newres = case r of
174+ Nothing -> res
175+ Just x -> x:res
176+ drainChildren chan (delete tid pending) newres
177+ PassOnResult er ->
178+ case er of
179+ Left e -> throwM e
180+ Right x -> drainChildren chan pending (x:res)
181+
182+waitForChildren :: (MonadIO m, MonadThrow m)
183+ => TChan (ChildEvent a) -> IORef [ThreadId] -> m [a]
170184waitForChildren chan pendingRef = do
171185 pending <- liftIO $ readIORef pendingRef
172− drainChildren chan pending
186+ r <- drainChildren chan pending []
173187 liftIO $ writeIORef pendingRef []
188+ return r
174189
175190------------------------------------------------------------------------------
176191-- Running the monad
177192------------------------------------------------------------------------------
178193
179−-- | Run a transient computation with a default initial state
180−runContext :: forall m a. MonadIO m => AsyncT m a -> m (Maybe a, Context)
181−runContext t = do
182− childChan <- liftIO $ atomically newTChan
183− pendingRef <- liftIO $ newIORef []
184− credit <- liftIO $ newIORef maxBound
185−
186− -- XXX this should be moved to Context.hs and then we can make m existential
187− -- and remove the unsafeCoerces
188− r <- runStateT (runAsyncT t) $ initContext
189− (empty :: AsyncT m a) childChan pendingRef credit
190−
191− waitForChildren childChan pendingRef
192− return r
193−
194−-- | Run an 'AsyncT m' computation. Returns the result of the computation or
195−-- may throw an exception. The computation finishes with a 'Nothing' result
196−-- when the input sources are exhausted.
197194-- XXX pass a collector function and return a Traversable.
198−-- Ideally it should be a non-empty list instead.
199−--waitAsync :: MonadIO m => AsyncT m a -> m [a]
200−waitAsync :: MonadIO m => AsyncT m a -> m (Maybe a)
201−waitAsync m = do
202− (r, _) <- runContext m
203− return r
195+-- XXX Ideally it should be a non-empty list instead.
196+-- | Run an 'AsyncT m' computation. Returns a list of results of the
197+-- computation or may throw an exception.
198+waitAsync :: forall m a. (MonadIO m, MonadThrow m, MonadCatch m)
199+ => AsyncT m a -> m [a]
200+waitAsync m = do
201+ childChan <- liftIO $ atomically newTChan
202+ pendingRef <- liftIO $ newIORef []
203+ credit <- liftIO $ newIORef maxBound
204+
205+ -- XXX this should be moved to Context.hs and then we can make m
206+ -- existential and remove the unsafeCoerces
207+ r <- try $ runStateT (runAsyncT m) $ initContext
208+ (empty :: AsyncT m a) childChan pendingRef credit
209+
210+ case r of
211+ Left (exc :: SomeException) -> do
212+ liftIO $ readIORef pendingRef >>= mapM_ killThread
213+ throwM exc
214+ Right (Nothing, _) -> waitForChildren childChan pendingRef
215+ Right ((Just x), _) -> return [x]
204216
205217------------------------------------------------------------------------------
206218-- * Extensible State: Session Data Management
src/Duct/Context.hs+13−9
@@ -3,7 +3,8 @@
33{-# LANGUAGE RecordWildCards #-}
44
55module Duct.Context
6− ( Context(..)
6+ ( ChildEvent(..)
7+ , Context(..)
78 , initContext
89 , saveContext
910 , restoreContext
@@ -18,9 +19,10 @@ module Duct.Context
1819 )
1920where
2021
21−import Control.Applicative (Alternative(..))
22+import Control.Applicative (Alternative(..))
2223import Control.Concurrent (ThreadId)
2324import Control.Concurrent.STM (TChan)
25+import Control.Exception (SomeException)
2426import Control.Monad.State (MonadState, gets, modify, StateT, MonadPlus)
2527import qualified Control.Monad.Trans.State.Lazy as Lazy (get, gets, modify, put)
2628import Data.Dynamic (TypeRep, Typeable, typeOf)
@@ -33,6 +35,10 @@ import GHC.Prim (Any)
3335-- State of a continuation
3436------------------------------------------------------------------------------
3537
38+data ChildEvent a =
39+ ChildDone ThreadId (Either SomeException (Maybe a)) -- A child is finished
40+ | PassOnResult (Either SomeException a) -- Pass on the result of a child
41+
3642-- | Describes the context of a computation.
3743data Context = Context
3844 { event :: Maybe () -- untyped, XXX rename to mailbox - can we do
@@ -53,10 +59,8 @@ data Context = Context
5359 , mfData :: M.Map TypeRep Any -- untyped, type coerced
5460 -- ^ State data accessed with get or put operations
5561
56− -- XXX All of the following can be removed
57− -- Even the remote status can be removed by the same logic
58−
59− , parentChannel :: Maybe (TChan ThreadId)
62+ -- XXX we can pass this at the time of fork rather than keeping it here.
63+ , parentChannel :: Maybe (TChan (ChildEvent Any))
6064 -- ^ Our parent thread's channel to communicate to when we die
6165
6266 -- Thread creation and cleanup handling. When a new thread is created the
@@ -69,7 +73,7 @@ data Context = Context
6973 -- parent thread waits on the channel until all its children are cleaned
7074 -- up.
7175
72− , childChannel :: TChan ThreadId
76+ , childChannel :: TChan (ChildEvent Any)
7377 -- ^ A channel for the immediate children to communicate to us when they die.
7478 -- Each thread has its own dedicated channel for its children
7579
@@ -100,7 +104,7 @@ data Context = Context
100104
101105initContext
102106 :: m a
103− -> TChan ThreadId
107+ -> TChan (ChildEvent a)
104108 -> IORef [ThreadId]
105109 -> IORef Int
106110 -> Context
@@ -111,7 +115,7 @@ initContext x childChan pending credit =
111115 , location = Worker
112116 , mfData = mempty
113117 , parentChannel = Nothing
114− , childChannel = childChan
118+ , childChannel = unsafeCoerce childChan
115119 , pendingThreads = pending
116120 , threadCredit = credit }
117121
src/Duct/Threads.hs+132−37
@@ -21,6 +21,7 @@ import Control.Concurrent.STM (TChan, atomically, newTChan,
2121import Control.Exception (ErrorCall (..),
2222 SomeException (..), catch)
2323import qualified Control.Exception.Lifted as EL
24+import Control.Monad.Catch (MonadThrow, throwM)
2425import Control.Monad.IO.Class (MonadIO (..))
2526import Control.Monad.State (get, gets, modify, put,
2627 runStateT, when)
@@ -71,7 +72,7 @@ import Duct.Context
7172resume :: Monad m => Context -> m (Maybe a)
7273resume ctx = do
7374 let s = runAsyncT (resumeContext ctx)
74− (a, s) <- runStateT s ctx
75+ (a, _) <- runStateT s ctx
7576 return a
7677
7778updateContextEvent :: Context -> a -> Context
@@ -81,25 +82,114 @@ updateContextEvent ctx evdata = ctx { event = Just $ unsafeCoerce evdata }
8182-- Thread Management (creation, reaping and killing)
8283------------------------------------------------------------------------------
8384
84−tryReclaimZombies :: MonadIO m => TChan ThreadId -> IORef [ThreadId] -> m ()
85−tryReclaimZombies chan pendingRef = do
85+-- XXX We are using unbounded channels so this will not block on writing to
86+-- pchan. We can use bounded channels to throttle the creation of threads based
87+-- on consumption rate.
88+processOneEvent :: MonadIO m
89+ => ChildEvent a
90+ -> TChan (ChildEvent a)
91+ -> [ThreadId]
92+ -> Maybe SomeException
93+ -> m ([ThreadId], Maybe SomeException)
94+processOneEvent ev pchan pending exc = do
95+ e <- case exc of
96+ Nothing ->
97+ case ev of
98+ ChildDone tid res -> do
99+ dbg $ "Reaping child: " ++ show tid
100+ handleDone res
101+ PassOnResult res -> handlePass res
102+ Just _ -> return exc
103+ let p = case ev of
104+ ChildDone tid _ -> delete tid pending
105+ _ -> pending
106+ return (p, e)
107+
108+ where
109+
110+ handleDone :: MonadIO m
111+ => Either SomeException (Maybe a) -> m (Maybe SomeException)
112+ handleDone res =
113+ case res of
114+ Left e -> do
115+ liftIO $ mapM_ killThread pending
116+ return (Just e)
117+ Right r -> do
118+ case r of
119+ Nothing -> return ()
120+ Just x -> liftIO $ atomically
121+ $ writeTChan pchan
122+ (PassOnResult (Right (unsafeCoerce x)))
123+ return Nothing
124+
125+ handlePass :: MonadIO m
126+ => Either SomeException a -> m (Maybe SomeException)
127+ handlePass res =
128+ case res of
129+ Left e -> do
130+ liftIO $ mapM_ killThread pending
131+ return (Just e)
132+ Right _ -> do
133+ liftIO $ atomically $ writeTChan pchan
134+ (PassOnResult (unsafeCoerce res))
135+ return Nothing
136+
137+tryReclaimZombies :: (MonadIO m, MonadThrow m) => Context -> m ()
138+tryReclaimZombies ctx = do
139+ let pchan = fromJust (parentChannel ctx)
140+ cchan = childChannel ctx
141+ pendingRef = pendingThreads ctx
142+
86143 pending <- liftIO $ readIORef pendingRef
87144 case pending of
88145 [] -> return ()
89146 _ -> do
90− mtid <- liftIO $ atomically $ tryReadTChan chan
91− case mtid of
147+ mev <- liftIO $ atomically $ tryReadTChan cchan
148+ case mev of
92149 Nothing -> return ()
93− Just tid -> do
94− liftIO $ writeIORef pendingRef $ delete tid pending
95− tryReclaimZombies chan pendingRef
96−
97−waitForOneChild :: MonadIO m => TChan ThreadId -> IORef [ThreadId] -> m ()
98−waitForOneChild chan pendingRef = do
150+ Just ev -> do
151+ (p, e) <- processOneEvent ev pchan pending Nothing
152+ liftIO $ writeIORef pendingRef p
153+ maybe (return ()) throwM e
154+ tryReclaimZombies ctx
155+
156+waitForOneEvent :: (MonadIO m, MonadThrow m) => Context -> m ()
157+waitForOneEvent ctx = do
99158 -- XXX assert pending must have at least one element
100159 -- assert that the tid is found in our list
101− tid <- liftIO $ atomically $ readTChan chan
102− liftIO $ modifyIORef pendingRef $ delete tid
160+ let pchan = fromJust (parentChannel ctx)
161+ cchan = childChannel ctx
162+ pendingRef = pendingThreads ctx
163+
164+ ev <- liftIO $ atomically $ readTChan cchan
165+ pending <- liftIO $ readIORef pendingRef
166+ (p, e) <- processOneEvent ev pchan pending Nothing
167+ liftIO $ writeIORef pendingRef p
168+ maybe (return ()) throwM e
169+
170+drainChildren :: MonadIO m
171+ => TChan (ChildEvent a)
172+ -> TChan (ChildEvent a)
173+ -> [ThreadId]
174+ -> Maybe SomeException
175+ -> m (Maybe SomeException)
176+drainChildren pchan cchan pending exc =
177+ if pending == []
178+ then return exc
179+ else do
180+ ev <- liftIO $ atomically $ readTChan cchan
181+ (p, e) <- processOneEvent ev pchan pending exc
182+ drainChildren pchan cchan p e
183+
184+waitForChildren :: MonadIO m
185+ => Context -> Maybe SomeException -> m (Maybe SomeException)
186+waitForChildren ctx exc = do
187+ let pendingRef = pendingThreads ctx
188+ pchan = fromJust (parentChannel ctx)
189+ pending <- liftIO $ readIORef pendingRef
190+ e <- drainChildren pchan (childChannel ctx) pending exc
191+ liftIO $ writeIORef pendingRef []
192+ return e
103193
104194-- | kill all the child threads associated with the continuation context
105195killChildren :: Context -> IO ()
@@ -148,7 +238,7 @@ forkFinally1 action and_then =
148238
149239-- | Run a given context in a new thread.
150240--
151−forkContextWith :: (MonadBaseControl IO m, MonadIO m)
241+forkContextWith :: (MonadBaseControl IO m, MonadIO m, MonadThrow m)
152242 => (Context -> m (Maybe a)) -> Context -> m ()
153243forkContextWith runCtx context = do
154244 child <- childContext context
@@ -157,12 +247,13 @@ forkContextWith runCtx context = do
157247
158248 where
159249
160− updatePendingThreads :: MonadIO m => Context -> ThreadId -> m ()
161− updatePendingThreads cur tid = do
250+ updatePendingThreads :: (MonadIO m, MonadThrow m)
251+ => Context -> ThreadId -> m ()
252+ updatePendingThreads ctx tid = do
162253 -- update the new thread before reclaiming zombies so that if it exited
163254 -- already reclaim finds it in the list and does not panic.
164− liftIO $ modifyIORef (pendingThreads cur) $ (\ts -> tid:ts)
165− tryReclaimZombies (childChannel cur) (pendingThreads cur)
255+ liftIO $ modifyIORef (pendingThreads ctx) $ (\ts -> tid:ts)
256+ tryReclaimZombies ctx
166257
167258 childContext ctx = do
168259 pendingRef <- liftIO $ newIORef []
@@ -174,25 +265,27 @@ forkContextWith runCtx context = do
174265 , childChannel = chan
175266 }
176267
177− beforeExit cur res = do
178− case res of
179− Left _exc -> liftIO $ killChildren cur
180− -- propagate the exception to the parent
181− Right _ -> -- collect the results?
182− return ()
268+ beforeExit ctx res = do
269+ exc <- case res of
270+ Left e -> do
271+ liftIO $ killChildren ctx
272+ return (Just e)
273+ Right _ -> return Nothing
183274
184− waitForChildren (childChannel cur) (pendingThreads cur)
185− signalQSemB (threadCredit cur)
275+ e <- waitForChildren ctx exc
276+
277+ tid <- myThreadId
186278 -- We are guaranteed to have a parent because we have been explicitly
187279 -- forked by some parent.
188− let p = fromJust (parentChannel cur)
189− tid <- myThreadId
190− liftIO $ atomically $ writeTChan p tid
280+ let p = fromJust (parentChannel ctx)
281+ signalQSemB (threadCredit ctx)
282+ liftIO $ atomically $ writeTChan p
283+ (ChildDone tid (maybe (unsafeCoerce res) Left e))
191284
192285-- | A wrapper to first decide whether to run the context in the same thread or
193286-- a new thread and then run the context in that thread.
194287--
195−resumeContextWith :: (MonadBaseControl IO m, MonadIO m)
288+resumeContextWith :: (MonadBaseControl IO m, MonadIO m, MonadThrow m)
196289 => (Context -> m (Maybe a)) -> Context -> m ()
197290resumeContextWith runCtx context = do
198291 gotCredit <- liftIO $ waitQSemB (threadCredit context)
@@ -208,8 +301,7 @@ resumeContextWith runCtx context = do
208301 -- special handling for those cases. Add those to
209302 -- unreclaimable list? And always execute them in an
210303 -- async thread, cannot use sync for those.
211− waitForOneChild (childChannel context)
212− (pendingThreads context)
304+ waitForOneEvent context
213305 resumeContextWith runCtx context
214306 True -> forkContextWith runCtx context
215307
@@ -238,7 +330,7 @@ data StreamData a =
238330--
239331-- | Execute the IO action, resume the saved context with the output of the io
240332-- action.
241−loopContextWith :: (MonadIO m, MonadBaseControl IO m)
333+loopContextWith :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
242334 => IO (StreamData t) -> Context -> m (Maybe a)
243335loopContextWith ioaction ctx = do
244336 streamData <- liftIO $ ioaction `catch`
@@ -260,7 +352,7 @@ loopContextWith ioaction ctx = do
260352-- Unless the maximum number of threads (set with 'threads') has been reached,
261353-- the task is generated in a new thread and the current thread returns a void
262354-- task.
263−parallel :: (Monad m, MonadIO m, MonadBaseControl IO m)
355+parallel :: (Monad m, MonadIO m, MonadBaseControl IO m, MonadThrow m)
264356 => IO (StreamData a) -> AsyncT m (StreamData a)
265357parallel ioaction = AsyncT $ do
266358 -- We retrieve the context here and pass it on so that we can resume it
@@ -291,7 +383,8 @@ parallel ioaction = AsyncT $ do
291383-- | An task stream generator that produces an infinite stream of tasks by
292384-- running an IO computation in a loop. A task is triggered carrying the output
293385-- of the computation. See 'parallel' for notes on the return value.
294−waitEvents :: (MonadIO m, MonadBaseControl IO m) => IO a -> AsyncT m a
386+waitEvents :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
387+ => IO a -> AsyncT m a
295388waitEvents io = do
296389 mr <- parallel (SMore <$> io)
297390 case mr of
@@ -301,7 +394,8 @@ waitEvents io = do
301394-- | Run an IO computation asynchronously and generate a single task carrying
302395-- the result of the computation when it completes. See 'parallel' for notes on
303396-- the return value.
304−async :: (MonadIO m, MonadBaseControl IO m) => IO a -> AsyncT m a
397+async :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
398+ => IO a -> AsyncT m a
305399async io = do
306400 mr <- parallel (SLast <$> io)
307401 case mr of
@@ -324,7 +418,8 @@ sync x = AsyncT $ do
324418-- task carries the result of the computation. A new task is generated only if
325419-- the output of the computation is different from the previous one. See
326420-- 'parallel' for notes on the return value.
327−sample :: (Eq a, MonadIO m, MonadBaseControl IO m) => IO a -> Int -> AsyncT m a
421+sample :: (Eq a, MonadIO m, MonadBaseControl IO m, MonadThrow m)
422+ => IO a -> Int -> AsyncT m a
328423sample action interval = do
329424 v <- liftIO action
330425 prev <- liftIO $ newIORef v