on master help sign in
Propagate exceptions from a concurrent tap

Data is tapped from a stream and folded in a separate thread. Any exceptions from that thread are propagated to the parent stream thread.

commit4ee8d64a8973eef82bb8da12fb94ce6bc585fca4 authorHarendra Kumar <harendra.kumar@gmail.com> dateFri 2 Aug 2019, 18:09 (7 years ago) parent2ab0d8f
4 files changed+234−51
src/Streamly/Internal/Data/SVar.hs+44−9 src/Streamly/Internal/Data/Stream/Async.hs+4 src/Streamly/Internal/Data/Stream/Parallel.hs+117−42 src/Streamly/Internal/Data/Stream/SVar.hs+69
src/Streamly/Internal/Data/SVar.hs+44−9
@@ -62,6 +62,7 @@ module Streamly.Internal.Data.SVar
6262 , ChildEvent (..)
6363 , AheadHeapEntry (..)
6464 , send
65+ , sendToProducer
6566 , sendYield
6667 , sendStop
6768 , enqueueLIFO
@@ -98,6 +99,7 @@ module Streamly.Internal.Data.SVar
9899 , postProcessPaced
99100 , readOutputQBounded
100101 , readOutputQPaced
102+ , readOutputQBasic
101103 , dispatchWorkerPaced
102104 , sendFirstWorker
103105 , delThread
@@ -109,6 +111,7 @@ module Streamly.Internal.Data.SVar
109111 , SVarStats (..)
110112 , dumpSVar
111113 , printSVar
114+ , withDiagMVar
112115 )
113116where
114117
@@ -424,6 +427,13 @@ data SVar t m a = SVar
424427 , readOutputQ :: m [ChildEvent a]
425428 , postProcess :: m Bool
426429
430+ -- channel to send events from the consumer to the worker. Used to send
431+ -- exceptions from a fold driver to the fold computation running as a
432+ -- consumer thread in the concurrent fold cases. Currently only one event
433+ -- is sent by the fold so we do not really need a queue for it.
434+ , outputQueueFromConsumer :: IORef ([ChildEvent a], Int)
435+ , outputDoorBellFromConsumer :: MVar ()
436+
427437 -- Combined/aggregate parameters
428438 -- This is truncated to maxBufferLimit if set to more than that. Otherwise
429439 -- potentially each worker may yield one value to the buffer in the worst
@@ -1055,13 +1065,12 @@ resetBufferLimit sv =
10551065 Limited n -> atomicModifyIORefCAS_ (pushBufferSpace sv)
10561066 (const (fromIntegral n))
10571067
1058−-- | This function is used by the producer threads to queue output for the
1059−-- consumer thread to consume. Returns whether the queue has more space.
1060−send :: SVar t m a -> ChildEvent a -> IO Int
1061−send sv msg = do
1062− -- XXX can the access to outputQueue and maxBufferLimit be made faster
1063− -- somehow?
1064− oldlen <- atomicModifyIORefCAS (outputQueue sv) $ \(es, n) ->
1068+{-# INLINE sendWithDoorBell #-}
1069+sendWithDoorBell ::
1070+ IORef ([ChildEvent a], Int) -> MVar () -> ChildEvent a -> IO Int
1071+sendWithDoorBell q bell msg = do
1072+ -- XXX can the access to outputQueue be made faster somehow?
1073+ oldlen <- atomicModifyIORefCAS q $ \(es, n) ->
10651074 ((msg : es, n + 1), n)
10661075 when (oldlen <= 0) $ do
10671076 -- The wake up must happen only after the store has finished otherwise
@@ -1073,9 +1082,25 @@ send sv msg = do
10731082 -- to read the queue again and find it empty.
10741083 -- The important point is that the consumer is guaranteed to receive a
10751084 -- doorbell if something was added to the queue after it empties it.
1076− void $ tryPutMVar (outputDoorBell sv) ()
1085+ void $ tryPutMVar bell ()
10771086 return oldlen
10781087
1088+-- | This function is used by the producer threads to queue output for the
1089+-- consumer thread to consume. Returns whether the queue has more space.
1090+send :: SVar t m a -> ChildEvent a -> IO Int
1091+send sv msg = sendWithDoorBell (outputQueue sv) (outputDoorBell sv) msg
1092+
1093+-- There is no bound implemented on the buffer, this is assumed to be low
1094+-- traffic.
1095+sendToProducer :: SVar t m a -> ChildEvent a -> IO Int
1096+sendToProducer sv msg = do
1097+ -- In case the producer stream is blocked on pushing to the fold buffer
1098+ -- then wake it up so that it can check for the stop event or exception
1099+ -- being sent to it otherwise we will be deadlocked.
1100+ void $ tryPutMVar (pushBufferMVar sv) ()
1101+ sendWithDoorBell (outputQueueFromConsumer sv)
1102+ (outputDoorBellFromConsumer sv) msg
1103+
10791104workerCollectLatency :: WorkerInfo -> IO (Maybe (Count, NanoSecond64))
10801105workerCollectLatency winfo = do
10811106 (cnt0, t0) <- readIORef (workerLatencyStart winfo)
@@ -2014,10 +2039,14 @@ sendWorkerWait delay dispatch sv = do
20142039-- Reading from the workers' output queue/buffer
20152040-------------------------------------------------------------------------------
20162041
2042+{-# INLINE readOutputQBasic #-}
2043+readOutputQBasic :: IORef ([ChildEvent a], Int) -> IO ([ChildEvent a], Int)
2044+readOutputQBasic q = atomicModifyIORefCAS q $ \x -> (([],0), x)
2045+
20172046{-# INLINE readOutputQRaw #-}
20182047readOutputQRaw :: SVar t m a -> IO ([ChildEvent a], Int)
20192048readOutputQRaw sv = do
2020− (list, len) <- atomicModifyIORefCAS (outputQueue sv) $ \x -> (([],0), x)
2049+ (list, len) <- readOutputQBasic (outputQueue sv)
20212050 when (svarInspectMode sv) $ do
20222051 let ref = maxOutQSize $ svarStats sv
20232052 oqLen <- readIORef ref
@@ -2211,6 +2240,7 @@ getAheadSVar st f mrun = do
22112240
22122241 let getSVar sv readOutput postProc = SVar
22132242 { outputQueue = outQ
2243+ , outputQueueFromConsumer = undefined
22142244 , remainingWork = yl
22152245 , maxBufferLimit = getMaxBuffer st
22162246 , pushBufferSpace = undefined
@@ -2219,6 +2249,7 @@ getAheadSVar st f mrun = do
22192249 , maxWorkerLimit = min (getMaxThreads st) (getMaxBuffer st)
22202250 , yieldRateInfo = rateInfo
22212251 , outputDoorBell = outQMv
2252+ , outputDoorBellFromConsumer = undefined
22222253 , readOutputQ = readOutput sv
22232254 , postProcess = postProc sv
22242255 , workerThreads = running
@@ -2280,7 +2311,9 @@ getParallelSVar :: MonadIO m
22802311 => SVarStopStyle -> State t m a -> RunInIO m -> IO (SVar t m a)
22812312getParallelSVar ss st mrun = do
22822313 outQ <- newIORef ([], 0)
2314+ outQRev <- newIORef ([], 0)
22832315 outQMv <- newEmptyMVar
2316+ outQMvRev <- newEmptyMVar
22842317 active <- newIORef 0
22852318 running <- newIORef S.empty
22862319 yl <- case getYieldLimit st of
@@ -2304,6 +2337,7 @@ getParallelSVar ss st mrun = do
23042337
23052338 let sv =
23062339 SVar { outputQueue = outQ
2340+ , outputQueueFromConsumer = outQRev
23072341 , remainingWork = yl
23082342 , maxBufferLimit = getMaxBuffer st
23092343 , pushBufferSpace = remBuf
@@ -2313,6 +2347,7 @@ getParallelSVar ss st mrun = do
23132347 -- Used only for diagnostics
23142348 , yieldRateInfo = rateInfo
23152349 , outputDoorBell = outQMv
2350+ , outputDoorBellFromConsumer = outQMvRev
23162351 , readOutputQ = readOutputQPar sv
23172352 , postProcess = allThreadsDone sv
23182353 , workerThreads = running
src/Streamly/Internal/Data/Stream/Async.hs+4
@@ -294,6 +294,7 @@ getLifoSVar st mrun = do
294294 -> SVar Stream m a
295295 getSVar sv readOutput postProc workDone wloop = SVar
296296 { outputQueue = outQ
297+ , outputQueueFromConsumer = undefined
297298 , remainingWork = yl
298299 , maxBufferLimit = getMaxBuffer st
299300 , pushBufferSpace = undefined
@@ -302,6 +303,7 @@ getLifoSVar st mrun = do
302303 , maxWorkerLimit = min (getMaxThreads st) (getMaxBuffer st)
303304 , yieldRateInfo = rateInfo
304305 , outputDoorBell = outQMv
306+ , outputDoorBellFromConsumer = undefined
305307 , readOutputQ = readOutput sv
306308 , postProcess = postProc sv
307309 , workerThreads = running
@@ -389,6 +391,7 @@ getFifoSVar st mrun = do
389391 -> SVar Stream m a
390392 getSVar sv readOutput postProc workDone wloop = SVar
391393 { outputQueue = outQ
394+ , outputQueueFromConsumer = undefined
392395 , remainingWork = yl
393396 , maxBufferLimit = getMaxBuffer st
394397 , pushBufferSpace = undefined
@@ -397,6 +400,7 @@ getFifoSVar st mrun = do
397400 , maxWorkerLimit = min (getMaxThreads st) (getMaxBuffer st)
398401 , yieldRateInfo = rateInfo
399402 , outputDoorBell = outQMv
403+ , outputDoorBellFromConsumer = undefined
400404 , readOutputQ = readOutput sv
401405 , postProcess = postProc sv
402406 , workerThreads = running
src/Streamly/Internal/Data/Stream/Parallel.hs+117−42
@@ -34,9 +34,9 @@ module Streamly.Internal.Data.Stream.Parallel
3434 )
3535where
3636
37−import Control.Concurrent (myThreadId)
38−import Control.Exception (SomeException(..), throwIO)
39−import Control.Monad (ap)
37+import Control.Concurrent (myThreadId, takeMVar)
38+import Control.Exception (SomeException(..))
39+import Control.Monad (ap, when)
4040import Control.Monad.Base (MonadBase(..), liftBaseDefault)
4141import Control.Monad.Catch (MonadThrow, throwM)
4242-- import Control.Monad.Error.Class (MonadError(..))
@@ -54,12 +54,13 @@ import Prelude hiding (map)
5454
5555import qualified Data.Set as Set
5656
57−import Streamly.Internal.Data.Stream.SVar (fromSVar, fromStreamVar)
57+import Streamly.Internal.Data.Stream.SVar
58+ (fromSVar, fromProducer, fromConsumer)
5859import Streamly.Internal.Data.Stream.Serial (map)
59−import Streamly.Internal.Data.Stream.StreamK (IsStream(..), Stream, mkStream, foldStream,
60− foldStreamShared, adapt)
60+import Streamly.Internal.Data.Stream.StreamK
61+ (IsStream(..), Stream, mkStream, foldStream, foldStreamShared, adapt)
6162
62−import Streamly.Internal.Data.SVar hiding (handleChildException)
63+import Streamly.Internal.Data.SVar
6364
6465import qualified Streamly.Internal.Data.Stream.StreamK as K
6566import qualified Streamly.Internal.Data.Stream.StreamD as D
@@ -274,49 +275,130 @@ foldParallel f m = do
274275 f $ fromSVar sv
275276
276277------------------------------------------------------------------------------
277−-- Parallel tee
278+-- Clone and distribute a stream in parallel
278279------------------------------------------------------------------------------
279280
281+-- push values to a fold worker via an SVar. Returns whether the fold is done.
282+{-# INLINE pushToFold #-}
283+pushToFold :: MonadAsync m => SVar Stream m a -> a -> m Bool
284+pushToFold sv a = do
285+ -- Check for exceptions before decrement so that we do not
286+ -- block forever if the child already exited with an exception.
287+ --
288+ -- We avoid a race between the consumer fold sending an event and we
289+ -- blocking on decrementBufferLimit by waking up the producer thread in
290+ -- sendToProducer before any event is sent by the fold to the producer
291+ -- stream.
292+ let qref = outputQueueFromConsumer sv
293+ done <- do
294+ (_, n) <- liftIO $ readIORef qref
295+ if (n > 0)
296+ then fromConsumer sv
297+ else return False
298+ if done
299+ then return True
300+ else liftIO $ do
301+ decrementBufferLimit sv
302+ void $ send sv (ChildYield a)
303+ return False
304+
305+-- Tap a stream and send the elements to the specified SVar in addition to
306+-- yielding them again.
307+--
308+-- XXX this could be written in StreamD style for better efficiency with fusion.
309+--
280310{-# INLINE teeToSVar #-}
281311teeToSVar :: (IsStream t, MonadAsync m) => SVar Stream m a -> t m a -> t m a
282312teeToSVar svr m = mkStream $ \st yld sng stp -> do
283− foldStreamShared st yld sng stp (go svr m)
313+ foldStreamShared st yld sng stp (go False m)
284314
285315 where
286316
287− go sv m0 = mkStream $ \st yld sng stp -> do
288− let stop = do
289− liftIO $ do
290− incrementBufferLimit sv
291− sendStop sv Nothing
292− -- XXX wait for Stop response event on exception channel,
293− -- drain the exception channel.
294− stp
295− sendit a = liftIO $ do
296− -- XXX check for exceptions before decrement so that we do not
297− -- block forever if the child already exited with an exception.
298− decrementBufferLimit sv
299− void $ send sv (ChildYield a)
300− single a = sendit a >> (liftIO $ sendStop sv Nothing) >> sng a
301− yieldk a r = sendit a >> yld a (go sv r)
317+ go False m0 = mkStream $ \st yld _ stp -> do
318+ let drain = do
319+ -- In general, a Stop event would come equipped with the result
320+ -- of the fold. It is not used here but it would be useful in
321+ -- applicative and distribute.
322+ done <- fromConsumer svr
323+ when (not done) $ do
324+ liftIO $ withDiagMVar svr "teeToSVar: waiting to drain"
325+ $ takeMVar (outputDoorBellFromConsumer svr)
326+ drain
327+
328+ stopFold = do
329+ liftIO $ sendStop svr Nothing
330+ -- drain/wait until a stop event arrives from the fold.
331+ drain
332+
333+ stop = stopFold >> stp
334+ single a = do
335+ done <- pushToFold svr a
336+ yld a (go done (K.nilM stopFold))
337+ yieldk a r = pushToFold svr a >>= \done -> yld a (go done r)
302338 in foldStreamShared st yieldk single stop m0
303339
340+ go True m0 = m0
341+
304342-- In case of folds the roles of worker and parent on an SVar are reversed. The
305343-- parent stream pushes values to an SVar instead of pulling from it and a
306344-- worker thread running the fold pulls from the SVar and folds the stream. We
307345-- keep a separate channel for pushing exceptions in the reverse direction i.e.
308346-- from the fold to the parent stream.
309347--
310−-- NOTE: If we use fromSVar here it will kill the main computation (the parent)
311−-- when the SVar goes away so we use fromStreamVar instead.
348+-- Note: If we terminate due to an exception, we do not actively terminate the
349+-- fold. It gets cleaned up by the GC.
350+
351+{-# NOINLINE handleFoldException #-}
352+handleFoldException :: SVar t m a -> SomeException -> IO ()
353+handleFoldException sv e = do
354+ tid <- myThreadId
355+ void $ sendToProducer sv (ChildStop tid (Just e))
356+
357+{-# NOINLINE sendStopToConsumer #-}
358+sendStopToConsumer :: MonadIO m => SVar t m a -> m ()
359+sendStopToConsumer sv = liftIO $ do
360+ tid <- myThreadId
361+ void $ sendToProducer sv (ChildStop tid Nothing)
362+
363+-- | Create an SVar with a fold consumer that will fold any elements sent to it
364+-- using the supplied fold function.
365+{-# INLINE newFoldSVar #-}
366+newFoldSVar :: (IsStream t, MonadAsync m)
367+ => State Stream m a -> (t m a -> m b) -> m (SVar Stream m a)
368+newFoldSVar stt f = do
369+ -- Buffer size for the SVar is derived from the current state
370+ sv <- newParallelVar StopAny (adaptState stt)
312371
313−{-# NOINLINE handleChildException #-}
314−handleChildException :: SVar t m a -> SomeException -> IO ()
315−handleChildException _sv e = do
316− -- tid <- myThreadId
317− -- void $ sendReverse sv (ChildStop tid (Just e))
318− throwIO e
372+ -- Add the producer thread-id to the SVar.
373+ liftIO myThreadId >>= modifyThread sv
319374
375+ -- A wrapper to send a Stop event back to the producer when the fold
376+ -- receives a stop from the producer.
377+ let pull m = mkStream $ \st yld _ stp -> do
378+ let stop = sendStopToConsumer sv >> stp
379+ single a = yld a (K.nilM (sendStopToConsumer sv))
380+ yieldk a r = yld a (pull r)
381+ in foldStreamShared st yieldk single stop m
382+
383+ void $ doFork (void $ f $ fromStream $ pull $ fromProducer sv)
384+ (svarMrun sv)
385+ (handleFoldException sv)
386+ return sv
387+
388+-- NOTE: In regular pull style streams, the consumer stream is pulling elements
389+-- from the SVar and we have several workers producing elements and pushing to
390+-- SVar. In case of folds, we, the parent stream driving the fold, are the
391+-- stream producing worker, we start an SVar and start pushing to the SVar, the
392+-- fold on the other side of the SVar is the consumer stream.
393+--
394+-- In the pull stream case exceptions are propagated from the producing workers
395+-- to the consumer stream, the exceptions are propagated on the same channel as
396+-- the produced stream elements. However, in case of push style folds the
397+-- current stream itself is the worker and the fold is the consumer, in this
398+-- case we have to propagate the exceptions from the consumer to the producer.
399+-- This is reverse of the pull case and we need a reverse direction channel
400+-- to propagate the exception.
401+--
320402-- | Redirect a copy of the stream to a supplied fold and run it concurrently
321403-- in an independent thread. The fold may buffer some elements. The buffer size
322404-- is determined by the prevailing 'maxBuffer' setting.
@@ -337,8 +419,8 @@ handleChildException _sv e = do
337419-- Exceptions from the concurrently running fold are propagated to the current
338420-- computation. Note that, because of buffering in the fold, exceptions may be
339421-- delayed and may not correspond to the current element being processed in the
340−-- parent stream, but we guarantee that the tap finishes and all exceptions
341−-- from it are drained before the parent stream stops.
422+-- parent stream, but we guarantee that before the parent stream stops the tap
423+-- finishes and all exceptions from it are drained.
342424--
343425--
344426-- Compare with 'tap'.
@@ -347,14 +429,7 @@ handleChildException _sv e = do
347429{-# INLINE tapAsync #-}
348430tapAsync :: (IsStream t, MonadAsync m) => (t m a -> m b) -> t m a -> t m a
349431tapAsync f m = mkStream $ \st yld sng stp -> do
350− -- Buffer size for the SVar is derived from the current state
351− sv <- newParallelVar StopNone (adaptState st)
352− -- XXX exception handling
353− -- XXX if we terminate due to an exception, do we need to actively
354− -- terminate the fold?
355− liftIO myThreadId >>= modifyThread sv
356− void $ doFork (void $ f $ fromStream $ fromStreamVar sv)
357− (svarMrun sv) (handleChildException sv)
432+ sv <- newFoldSVar st f
358433 foldStreamShared st yld sng stp (teeToSVar sv m)
359434
360435------------------------------------------------------------------------------
src/Streamly/Internal/Data/Stream/SVar.hs+69
@@ -14,6 +14,8 @@
1414module Streamly.Internal.Data.Stream.SVar
1515 ( fromSVar
1616 , fromStreamVar
17+ , fromProducer
18+ , fromConsumer
1719 , toSVar
1820 )
1921where
@@ -108,3 +110,70 @@ fromSVar sv =
108110-- be read back from the SVar using 'fromSVar'.
109111toSVar :: (IsStream t, MonadAsync m) => SVar Stream m a -> t m a -> m ()
110112toSVar sv m = toStreamVar sv (toStream m)
113+
114+-------------------------------------------------------------------------------
115+-- Process events received by a fold consumer from a stream producer
116+-------------------------------------------------------------------------------
117+
118+-- | Pull a stream from an SVar.
119+{-# NOINLINE fromProducer #-}
120+fromProducer :: MonadAsync m => SVar Stream m a -> Stream m a
121+fromProducer sv = mkStream $ \st yld sng stp -> do
122+ list <- readOutputQ sv
123+ -- Reversing the output is important to guarantee that we process the
124+ -- outputs in the same order as they were generated by the constituent
125+ -- streams.
126+ foldStream st yld sng stp $ processEvents $ reverse list
127+
128+ where
129+
130+ allDone stp = do
131+ when (svarInspectMode sv) $ do
132+ t <- liftIO $ getTime Monotonic
133+ liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
134+ liftIO $ printSVar sv "SVar Done"
135+ stp
136+
137+ {-# INLINE processEvents #-}
138+ processEvents [] = mkStream $ \st yld sng stp -> do
139+ foldStream st yld sng stp $ fromProducer sv
140+
141+ processEvents (ev : es) = mkStream $ \_ yld _ stp -> do
142+ let rest = processEvents es
143+ case ev of
144+ ChildYield a -> yld a rest
145+ ChildStop tid e -> do
146+ accountThread sv tid
147+ case e of
148+ Nothing -> allDone stp
149+ Just _ -> error "Bug: fromProducer: received exception"
150+
151+-------------------------------------------------------------------------------
152+-- Process events received by the producer thread from the consumer side
153+-------------------------------------------------------------------------------
154+
155+-- XXX currently only one event is sent by a fold consumer to the stream
156+-- producer. But we can potentially have multiple events e.g. the fold step can
157+-- generate exception more than once and the producer can ignore those
158+-- exceptions or handle them and still keep driving the fold.
159+--
160+{-# NOINLINE fromConsumer #-}
161+fromConsumer :: MonadAsync m => SVar Stream m a -> m Bool
162+fromConsumer sv = do
163+ (list, _) <- liftIO $ readOutputQBasic (outputQueueFromConsumer sv)
164+ -- Reversing the output is important to guarantee that we process the
165+ -- outputs in the same order as they were generated by the constituent
166+ -- streams.
167+ processEvents $ reverse list
168+
169+ where
170+
171+ {-# INLINE processEvents #-}
172+ processEvents [] = return False
173+ processEvents (ev : _) = do
174+ case ev of
175+ ChildStop _ e -> do
176+ case e of
177+ Nothing -> return True
178+ Just ex -> throwM ex
179+ ChildYield _ -> error "Bug: fromConsumer: invalid ChildYield event"