on master help sign in
Stop the channel when a particular stream ends
commit6c4b36105d01c44163d52003052b227789fe943d authorHarendra Kumar <harendra@composewell.com> dateSat 8 Oct 2022, 04:45 (4 years ago) parent18e656f
5 files changed+185−43
src/Streamly/Internal/Data/Stream/Async.hs+32−1 src/Streamly/Internal/Data/Stream/Async/Channel.hs+73−24 src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hs+22−11 src/Streamly/Internal/Data/Stream/Async/Channel/Type.hs+19−3 src/Streamly/Internal/Data/Stream/Channel/Types.hs+39−4
src/Streamly/Internal/Data/Stream/Async.hs+32−1
@@ -29,6 +29,8 @@ module Streamly.Internal.Data.Stream.Async
2929 , maxThreads
3030 , maxBuffer
3131 , eagerEval
32+ , StopWhen (..)
33+ , stopWhen
3234 -- maxYields
3335 , Rate(..)
3436 , rate
@@ -41,7 +43,6 @@ module Streamly.Internal.Data.Stream.Async
4143 -- * Combinators
4244 -- | Stream combinators using Async channel
4345
44− -- XXX Move to parallel module
4546 , eval
4647 , evalWith
4748
@@ -55,6 +56,9 @@ module Streamly.Internal.Data.Stream.Async
5556 , appendWith
5657 , interleave
5758 , interleaveWith
59+ , parallel
60+ , parallelFst
61+ , parallelMin
5862
5963 , apply
6064 , applyWith
@@ -173,6 +177,33 @@ append = appendWith id
173177interleave :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
174178interleave = interleaveWith id
175179
180+-- | Like 'append' but with 'eagerEval' on.
181+--
182+-- >>> parallel = Async.appendWith Async.eagerEval
183+--
184+{-# INLINE parallel #-}
185+parallel :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
186+parallel = appendWith eagerEval
187+
188+-- | Like 'parallel' but stops the output as soon as the first stream stops.
189+--
190+-- >>> parallelFst = Async.appendWith (Async.eagerEval . Async.stopWhen Async.FirstStops)
191+--
192+-- /Pre-release/
193+{-# INLINE parallelFst #-}
194+parallelFst :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
195+parallelFst = appendWith (eagerEval . stopWhen FirstStops)
196+
197+-- | Like 'parallel' but stops the output as soon as any of the two streams
198+-- stops.
199+--
200+-- >>> parallelMin = Async.appendWith (Async.eagerEval . Async.stopWhen Async.AnyStops)
201+--
202+-- /Pre-release/
203+{-# INLINE parallelMin #-}
204+parallelMin :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
205+parallelMin = appendWith (eagerEval . stopWhen AnyStops)
206+
176207-- concatMapWith modifier f stream = concatWith modifier $ fmap f stream
177208
178209-- | Like 'concatMap' but we can also specify the concurrent channel's
src/Streamly/Internal/Data/Stream/Async/Channel.hs+73−24
@@ -23,6 +23,8 @@ module Streamly.Internal.Data.Stream.Async.Channel
2323 , maxRate
2424 , constRate
2525 , inspectMode
26+ , StopWhen (..)
27+ , stopWhen
2628
2729 -- * Operations
2830 , fromChannel
@@ -129,35 +131,46 @@ evalWithD modifier m = D.Stream step Nothing
129131-- appending two streams
130132-------------------------------------------------------------------------------
131133
132−-- | Create a new channel and add both the streams to it for async evaluation.
133−-- The output stream is the result of the evaluation.
134−{-# INLINE appendWithK #-}
135−appendWithK :: MonadAsync m =>
136− (Config -> Config) -> K.Stream m a -> K.Stream m a -> K.Stream m a
137−appendWithK modifier stream1 stream2 = K.concatEffect action
134+{-# INLINE appendGeneric #-}
135+appendGeneric :: MonadAsync m =>
136+ ((Config -> Config) -> m (Channel m a))
137+ -> (Config -> Config)
138+ -> K.Stream m a
139+ -> K.Stream m a
140+ -> K.Stream m a
141+appendGeneric newChan modifier stream1 stream2 = K.concatEffect action
138142
139143 where
140144
141145 action = do
142− chan <- Append.newChannel modifier
143− toChannelK chan stream2
144− toChannelK chan stream1
146+ chan <- newChan modifier
147+ let cfg = modifier defaultConfig
148+ done = K.nilM (stopChannel chan)
149+ case getStopWhen cfg of
150+ AllStop -> do
151+ toChannelK chan stream2
152+ toChannelK chan stream1
153+ FirstStops -> do
154+ toChannelK chan stream2
155+ toChannelK chan (K.serial stream1 done)
156+ AnyStops -> do
157+ toChannelK chan (K.serial stream2 done)
158+ toChannelK chan (K.serial stream1 done)
145159 return $ Stream.toStreamK $ fromChannel chan
146160
161+-- | Create a new channel and add both the streams to it for async evaluation.
162+-- The output stream is the result of the evaluation.
163+{-# INLINE appendWithK #-}
164+appendWithK :: MonadAsync m =>
165+ (Config -> Config) -> K.Stream m a -> K.Stream m a -> K.Stream m a
166+appendWithK = appendGeneric Append.newChannel
167+
147168-- | Create a new channel and add both the streams to it for async evaluation.
148169-- The output stream is the result of the evaluation.
149170{-# INLINE interleaveWithK #-}
150171interleaveWithK :: MonadAsync m =>
151172 (Config -> Config) -> K.Stream m a -> K.Stream m a -> K.Stream m a
152−interleaveWithK modifier stream1 stream2 = K.concatEffect action
153−
154− where
155−
156− action = do
157− chan <- Interleave.newChannel modifier
158− toChannelK chan stream2
159− toChannelK chan stream1
160− return $ Stream.toStreamK $ fromChannel chan
173+interleaveWithK = appendGeneric Interleave.newChannel
161174
162175-- | Evaluate the first stream in the current thread and add the second stream
163176-- to the supplied channel. This is to be used by a worker thread.
@@ -219,26 +232,62 @@ concatMapWithChanK chan f stream =
219232 in K.concatMapEffect (`run` stream) (mkEnqueue chan run)
220233 -- K.concatMapWith (_appendWithChanK chan) f stream
221234
235+{-# INLINE concatMapWithChanKAny #-}
236+concatMapWithChanKAny :: MonadAsync m =>
237+ Channel m b -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
238+concatMapWithChanKAny chan f stream =
239+ let done = K.nilM (stopChannel chan)
240+ run q = concatMapDivK q (\x -> K.serial (f x) done)
241+ in K.concatMapEffect (`run` stream) (mkEnqueue chan run)
242+
243+{-# INLINE concatMapWithChanKFirst #-}
244+concatMapWithChanKFirst :: MonadAsync m =>
245+ Channel m b -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
246+concatMapWithChanKFirst chan f stream =
247+ let done = K.nilM (stopChannel chan)
248+ run q = concatMapDivK q f
249+ in K.concatEffect $ do
250+ res <- K.uncons stream
251+ case res of
252+ Nothing -> return K.nil
253+ Just (h, t) -> do
254+ q <- mkEnqueue chan run
255+ q t
256+ return $ K.serial (f h) done
257+
258+{-# INLINE concatMapWithChanKGeneric #-}
259+concatMapWithChanKGeneric :: MonadAsync m =>
260+ (Config -> Config)
261+ -> Channel m b
262+ -> (a -> K.Stream m b)
263+ -> K.Stream m a
264+ -> K.Stream m b
265+concatMapWithChanKGeneric modifier chan f stream = do
266+ let cfg = modifier defaultConfig
267+ case getStopWhen cfg of
268+ AllStop -> concatMapWithChanK chan f stream
269+ FirstStops -> concatMapWithChanKFirst chan f stream
270+ AnyStops -> concatMapWithChanKAny chan f stream
271+
222272-- XXX Add a deep evaluation variant that evaluates individual elements in the
223273-- generated streams in parallel.
224274
225275-- | Allocate a channel and use it to concurrently evaluate the streams
226276-- generated by the mapped function.
227277--
228−-- >> concatMapWithK modifier f = withChannelK (`concatMapWithChanK` f) modifier
229−--
230278{-# INLINE concatMapWithK #-}
231279concatMapWithK :: MonadAsync m =>
232280 (Config -> Config) -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
233−concatMapWithK modifier f = withChannelK (`concatMapWithChanK` f) modifier
281+concatMapWithK modifier f =
282+ let g = concatMapWithChanKGeneric modifier
283+ in withChannelK (`g` f) modifier
234284
235285-- | Allocate a channel and use it to concurrently evaluate the streams
236286-- generated by the mapped function.
237287--
238−-- >> concatMapInterleaveWithK modifier f = withInterleaveChannelK (`concatMapWithChanK` f) modifier
239−--
240288{-# INLINE concatMapInterleaveWithK #-}
241289concatMapInterleaveWithK :: MonadAsync m =>
242290 (Config -> Config) -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
243291concatMapInterleaveWithK modifier f =
244− withInterleaveChannelK (`concatMapWithChanK` f) modifier
292+ let g = concatMapWithChanKGeneric modifier
293+ in withInterleaveChannelK (`g` f) modifier
src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hs+22−11
@@ -129,18 +129,17 @@ fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
129129
130130 where
131131
132− allDone stp = do
133− when (svarInspectMode sv) $ do
134− t <- liftIO $ getTime Monotonic
135− liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
136− liftIO $ printSVar (dumpSVar sv) "SVar Done"
137− stp
132+ cleanup = do
133+ when (svarInspectMode sv) $ liftIO $ do
134+ t <- getTime Monotonic
135+ writeIORef (svarStopTime (svarStats sv)) (Just t)
136+ printSVar (dumpSVar sv) "SVar Done"
138137
139138 {-# INLINE processEvents #-}
140139 processEvents [] = K.MkStream $ \st yld sng stp -> do
141140 done <- postProcess sv
142141 if done
143− then allDone stp
142+ then cleanup >> stp
144143 else K.foldStream st yld sng stp $ fromChannelRaw sv
145144
146145 processEvents (ev : es) = K.MkStream $ \st yld sng stp -> do
@@ -153,11 +152,23 @@ fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
153152 Nothing -> K.foldStream st yld sng stp rest
154153 Just ex ->
155154 case fromException ex of
156− Just ThreadAbort ->
157− K.foldStream st yld sng stp rest
158− Nothing -> do
155+ Just ChannelStop -> do
159156 liftIO (cleanupSVar (workerThreads sv))
160− throwM ex
157+ cleanup >> stp
158+ Nothing -> do
159+ case fromException ex of
160+ Just ThreadAbort ->
161+ -- We terminate the loop after sending
162+ -- ThreadAbort to workers so we should
163+ -- never get it unless it is thrown
164+ -- from inside a worker thread or by
165+ -- someone else to our thread.
166+ error "processEvents: got ThreadAbort"
167+ -- K.foldStream st yld sng stp rest
168+ Nothing -> do
169+ -- Throw ThreadAbort to all and exit.
170+ liftIO (cleanupSVar (workerThreads sv))
171+ cleanup >> throwM ex
161172
162173#if __GLASGOW_HASKELL__ < 810
163174#ifdef INSPECTION
src/Streamly/Internal/Data/Stream/Async/Channel/Type.hs+19−3
@@ -11,19 +11,24 @@ module Streamly.Internal.Data.Stream.Async.Channel.Type
1111 Channel(..)
1212 , yield
1313 , stop
14+ , stopChannel
1415 , dumpSVar
1516 )
1617where
1718
18−import Control.Concurrent (ThreadId)
19+import Control.Concurrent (ThreadId, myThreadId)
1920import Control.Concurrent.MVar (MVar)
21+import Control.Exception (toException)
22+import Control.Monad (void)
23+import Control.Monad.IO.Class (MonadIO(..))
2024import Data.IORef (IORef)
2125import Data.List (intersperse)
2226import Data.Set (Set)
23−
2427import Streamly.Internal.Control.Concurrent (RunInIO)
28+import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS_)
2529import Streamly.Internal.Data.Stream.Channel.Dispatcher (dumpSVarStats)
26−import Streamly.Internal.Data.Stream.Channel.Worker (sendYield, sendStop)
30+import Streamly.Internal.Data.Stream.Channel.Worker
31+ (sendYield, sendStop, sendWithDoorBell)
2732import Streamly.Internal.Data.Stream.StreamK.Type (Stream)
2833
2934import Streamly.Internal.Data.Stream.Channel.Types
@@ -137,6 +142,17 @@ stop sv winfo =
137142 (outputQueue sv)
138143 (outputDoorBell sv)
139144
145+{-# INLINABLE stopChannel #-}
146+stopChannel :: MonadIO m => Channel m a -> m ()
147+stopChannel chan = liftIO $ do
148+ atomicModifyIORefCAS_ (workerCount chan) $ \n -> n - 1
149+ myThreadId >>= \tid ->
150+ void
151+ $ sendWithDoorBell
152+ (outputQueue chan)
153+ (outputDoorBell chan)
154+ (ChildStop tid (Just (toException ChannelStop)))
155+
140156{-# NOINLINE dumpSVar #-}
141157dumpSVar :: Channel m a -> IO String
142158dumpSVar sv = do
src/Streamly/Internal/Data/Stream/Channel/Types.hs+39−4
@@ -37,6 +37,7 @@ module Streamly.Internal.Data.Stream.Channel.Types
3737 Count (..)
3838 , Limit (..)
3939 , ThreadAbort (..)
40+ , ChannelStop (..)
4041 , ChildEvent (..)
4142
4243 -- * Stats
@@ -60,6 +61,7 @@ module Streamly.Internal.Data.Stream.Channel.Types
6061
6162 -- * Configuration
6263 , Rate (..)
64+ , StopWhen (..)
6365 , Config
6466
6567 -- ** Default config
@@ -72,6 +74,7 @@ module Streamly.Internal.Data.Stream.Channel.Types
7274 , maxYields
7375 , inspectMode
7476 , eagerEval
77+ , stopWhen
7578
7679 , rate
7780 , avgRate
@@ -88,6 +91,7 @@ module Streamly.Internal.Data.Stream.Channel.Types
8891 , getYieldLimit
8992 , getInspectMode
9093 , getEagerDispatch
94+ , getStopWhen
9195
9296 -- * Cleanup
9397 , cleanupSVar
@@ -107,7 +111,7 @@ module Streamly.Internal.Data.Stream.Channel.Types
107111 )
108112where
109113
110−import Control.Concurrent (ThreadId, myThreadId, throwTo, MVar, tryReadMVar)
114+import Control.Concurrent (ThreadId, throwTo, MVar, tryReadMVar)
111115import Control.Concurrent.MVar (tryPutMVar)
112116import Control.Exception
113117 ( SomeException(..), Exception, catches, throwIO, Handler(..)
@@ -163,10 +167,19 @@ instance Ord Limit where
163167-- Parent child thread communication type
164168------------------------------------------------------------------------------
165169
170+-- | Channel driver throws this exception to all active workers to clean up
171+-- the channel.
166172data ThreadAbort = ThreadAbort deriving Show
167173
168174instance Exception ThreadAbort
169175
176+-- | If any stream in the channel throws this exception, the channel stops
177+-- immediately.
178+data ChannelStop = ChannelStop deriving Show
179+
180+instance Exception ChannelStop
181+
182+-- XXX Use a ChildSingle event to speed up mapM?
170183-- | Events that a child thread may send to a parent thread.
171184data ChildEvent a =
172185 ChildYield a
@@ -298,6 +311,14 @@ data Rate = Rate
298311 , rateBuffer :: Int -- ^ Maximum slack from the goal
299312 }
300313
314+-- | Specify when the 'Channel' should stop. Note that for ordered streams only
315+-- 'FirstStops' and 'AnyStop' would be the same, and usually only 'AllStop'
316+-- would make sense. This option is most useful for eager unordered streams.
317+data StopWhen =
318+ FirstStops -- ^ Stop when the first stream/action ends.
319+ | AllStop -- ^ Stop when all the streams end.
320+ | AnyStops -- ^ Stop when any one stream ends.
321+
301322-- XXX we can put the resettable fields in a oneShotConfig field and others in
302323-- a persistentConfig field. That way reset would be fast and scalable
303324-- irrespective of the number of fields.
@@ -323,6 +344,7 @@ data Config = Config
323344 , _maxStreamRate :: Maybe Rate
324345 , _inspectMode :: Bool
325346 , _eagerDispatch :: Bool
347+ , _stopWhen :: StopWhen
326348 }
327349
328350-------------------------------------------------------------------------------
@@ -354,6 +376,7 @@ defaultConfig = Config
354376 , _inspectMode = False
355377 -- XXX Set it to True when Rate is not set?
356378 , _eagerDispatch = False
379+ , _stopWhen = AllStop
357380 }
358381
359382-------------------------------------------------------------------------------
@@ -463,6 +486,8 @@ setStreamLatency n st =
463486getStreamLatency :: Config -> Maybe NanoSecond64
464487getStreamLatency = _streamLatency
465488
489+-- XXX Rename to "inspect"
490+
466491-- | Print debug information about the 'Channel' when the stream ends.
467492--
468493-- /Pre-release/
@@ -473,6 +498,8 @@ inspectMode st = st { _inspectMode = True }
473498getInspectMode :: Config -> Bool
474499getInspectMode = _inspectMode
475500
501+-- XXX rename to "eager"
502+
476503-- | By default the stream is evaluated on demand, new evaluation may be
477504-- deferred until the consumer consumes the evaluated stream. When 'eagerEval'
478505-- option is on the evaluation is aggressive and does not wait for consumption
@@ -487,6 +514,13 @@ eagerEval st = st { _eagerDispatch = True }
487514getEagerDispatch :: Config -> Bool
488515getEagerDispatch = _eagerDispatch
489516
517+-- | Specify when the 'Channel' should stop.
518+stopWhen :: StopWhen -> Config -> Config
519+stopWhen cond st = st { _stopWhen = cond }
520+
521+getStopWhen :: Config -> StopWhen
522+getStopWhen = _stopWhen
523+
490524-------------------------------------------------------------------------------
491525-- Initialization
492526-------------------------------------------------------------------------------
@@ -740,13 +774,14 @@ printSVar dump how = do
740774-- Cleanup
741775-------------------------------------------------------------------------------
742776
743−-- Can be called from a worker as well, avoids throwing to itself
777+-- | Never called from a worker thread.
744778cleanupSVar :: IORef (Set ThreadId) -> IO ()
745779cleanupSVar workerSet = do
746780 workers <- readIORef workerSet
747− self <- myThreadId
781+ -- self <- myThreadId
748782 Prelude.mapM_ (`throwTo` ThreadAbort)
749− (Prelude.filter (/= self) $ Set.toList workers)
783+ -- (Prelude.filter (/= self) $ Set.toList workers)
784+ (Set.toList workers)
750785
751786-------------------------------------------------------------------------------
752787-- Evaluator