on master help sign in
Keep IO exceptions in streamly-core and lifted in streamly
commit230e7e334a05ed24af51477268865845ffadc3b3 authorHarendra Kumar <harendra@composewell.com> dateSun 23 Oct 2022, 03:57 (4 years ago) parent756f8c1
8 files changed+324−11
benchmark/Streamly/Benchmark/Data/Stream/Exceptions.hs+1 core/src/Streamly/Data/Stream.hs+3−3 core/src/Streamly/Internal/Data/Stream/Exception.hs+76−4 core/src/Streamly/Internal/Data/Stream/StreamD/Exception.hs−1 src/Streamly/Data/Stream/Exception.hs+20 src/Streamly/Internal/Data/Stream/Exception/Lifted.hs+221 src/Streamly/Internal/FileSystem/Event/Windows.hs+2−3 streamly.cabal+1
benchmark/Streamly/Benchmark/Data/Stream/Exceptions.hs+1
@@ -45,6 +45,7 @@ import qualified Streamly.Internal.Data.Unfold as IUF
4545import qualified Streamly.Internal.Data.Stream.IsStream as Stream
4646#else
4747import qualified Streamly.Internal.Data.Stream as Stream
48+import qualified Streamly.Internal.Data.Stream.Exception as Stream
4849#endif
4950
5051import Gauge hiding (env)
core/src/Streamly/Data/Stream.hs+3−3
@@ -437,9 +437,9 @@ module Streamly.Data.Stream
437437 -- | 'bracket' is the most general resource management operation, all other
438438 -- operations can be expressed using it.
439439 , before
440− , after
441− , finally
442− , bracket
440+ , afterIO
441+ , finallyIO
442+ , bracketIO
443443
444444 -- * Lifting Inner Monad
445445 -- | See also: "Streamly.Internal.Data.Stream.Lift" for
core/src/Streamly/Internal/Data/Stream/Exception.hs+76−4
@@ -11,12 +11,16 @@ module Streamly.Internal.Data.Stream.Exception
1111 before
1212 , after_
1313 , after
14+ , afterIO
1415 , bracket_
1516 , bracket
17+ , bracketIO
1618 , bracket'
19+ , bracket3IO
1720 , onException
1821 , finally_
1922 , finally
23+ , finallyIO
2024 , ghandle
2125 , handle
2226 , retry
@@ -83,6 +87,18 @@ after :: (MonadIO m, MonadBaseControl IO m)
8387 => m b -> Stream m a -> Stream m a
8488after action xs = fromStreamD $ D.after action $ toStreamD xs
8589
90+-- | Run the action @IO b@ whenever the stream stream stops normally, or
91+-- if it is garbage collected after a partial lazy evaluation.
92+--
93+-- The semantics of the action @IO b@ are similar to the semantics of cleanup
94+-- action in 'bracketIO'.
95+--
96+-- /See also 'after_'/
97+--
98+{-# INLINE afterIO #-}
99+afterIO :: IO b -> Stream IO a -> Stream IO a
100+afterIO = after
101+
86102-- | Run the action @m b@ if the stream aborts due to an exception. The
87103-- exception is not caught, simply rethrown.
88104--
@@ -97,8 +113,7 @@ onException action xs = fromStreamD $ D.onException action $ toStreamD xs
97113--
98114-- * action @m b@ won't run if the stream is garbage collected
99115-- after partial evaluation.
100−-- * does not require a 'MonadAsync' constraint.
101−-- * has slightly better performance than 'finally'.
116+-- * has slightly better performance than 'finallyIO'.
102117--
103118-- /Inhibits stream fusion/
104119--
@@ -127,13 +142,29 @@ finally :: (MonadIO m, MonadBaseControl IO m, MonadCatch m) =>
127142 m b -> Stream m a -> Stream m a
128143finally action xs = fromStreamD $ D.finally action $ toStreamD xs
129144
145+-- | Run the action @IO b@ whenever the stream stream stops normally, aborts
146+-- due to an exception or if it is garbage collected after a partial lazy
147+-- evaluation.
148+--
149+-- The semantics of running the action @IO b@ are similar to the cleanup action
150+-- semantics described in 'bracketIO'.
151+--
152+-- >>> finallyIO release = Stream.bracketIO (return ()) (const release)
153+--
154+-- /See also 'finally_'/
155+--
156+-- /Inhibits stream fusion/
157+--
158+{-# INLINE finallyIO #-}
159+finallyIO :: IO b -> Stream IO a -> Stream IO a
160+finallyIO = finally
161+
130162-- | Like 'bracket' but with following differences:
131163--
132164-- * alloc action @m b@ runs with async exceptions enabled
133165-- * cleanup action @b -> m c@ won't run if the stream is garbage collected
134166-- after partial evaluation.
135−-- * does not require a 'MonadAsync' constraint.
136−-- * has slightly better performance than 'bracket'.
167+-- * has slightly better performance than 'bracketIO'.
137168--
138169-- /Inhibits stream fusion/
139170--
@@ -194,6 +225,47 @@ bracket' :: (MonadIO m, MonadBaseControl IO m, MonadCatch m)
194225bracket' bef aft gc exc bet = fromStreamD $
195226 D.bracket' bef aft exc gc (toStreamD . bet)
196227
228+-- | Like 'bracketIO' but can use separate cleanup actions depending on the mode
229+-- of termination. @bracket3IO before onStop onGC onException action@ runs
230+-- @action@ using the result of @before@. If the stream stops, @onStop@ action
231+-- is executed, if the stream is abandoned @onGC@ is executed, if the stream
232+-- encounters an exception @onException@ is executed.
233+--
234+-- /Pre-release/
235+{-# INLINE bracket3IO #-}
236+bracket3IO :: IO b
237+ -> (b -> IO c)
238+ -> (b -> IO d)
239+ -> (b -> IO e)
240+ -> (b -> Stream IO a)
241+ -> Stream IO a
242+bracket3IO = bracket'
243+
244+-- | Run the alloc action @IO b@ with async exceptions disabled but keeping
245+-- blocking operations interruptible (see 'Control.Exception.mask'). Use the
246+-- output @b@ as input to @b -> Stream IO a@ to generate an output stream.
247+--
248+-- @b@ is usually a resource under the IO monad, e.g. a file handle, that
249+-- requires a cleanup after use. The cleanup action @b -> IO c@, runs whenever
250+-- the stream ends normally, due to a sync or async exception or if it gets
251+-- garbage collected after a partial lazy evaluation.
252+--
253+-- 'bracketIO' only guarantees that the cleanup action runs, and it runs with
254+-- async exceptions enabled. The action must ensure that it can successfully
255+-- cleanup the resource in the face of sync or async exceptions.
256+--
257+-- When the stream ends normally or on a sync exception, cleanup action runs
258+-- immediately in the current thread context, whereas in other cases it runs in
259+-- the GC context, therefore, cleanup may be delayed until the GC gets to run.
260+--
261+-- /See also: 'bracket_'/
262+--
263+-- /Inhibits stream fusion/
264+--
265+{-# INLINE bracketIO #-}
266+bracketIO :: IO b -> (b -> IO c) -> (b -> Stream IO a) -> Stream IO a
267+bracketIO bef aft = bracket3IO bef aft aft aft
268+
197269-- | Like 'handle' but the exception handler is also provided with the stream
198270-- that generated the exception as input. The exception handler can thus
199271-- re-evaluate the stream to retry the action that failed. The exception
core/src/Streamly/Internal/Data/Stream/StreamD/Exception.hs−1
@@ -49,7 +49,6 @@ data GbracketState s1 s2 v
4949-- * alloc action @m c@ runs with async exceptions enabled
5050-- * cleanup action @c -> m d@ won't run if the stream is garbage collected
5151-- after partial evaluation.
52−-- * does not require a 'MonadAsync' constraint.
5352--
5453-- /Inhibits stream fusion/
5554--
src/Streamly/Data/Stream/Exception.hsnew file+20
@@ -0,0 +1,20 @@
1+-- |
2+-- Module : Streamly.Data.Stream.Exception
3+-- Copyright : (c) 2022 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : released
7+-- Portability : GHC
8+--
9+-- >>> import qualified Streamly.Data.Stream.Exception as Stream
10+--
11+module Streamly.Data.Stream.Exception
12+ (
13+ after
14+ , bracket
15+ , bracket3
16+ , finally
17+ )
18+where
19+
20+import qualified Streamly.Internal.Data.Stream.Exception
src/Streamly/Internal/Data/Stream/Exception/Lifted.hsnew file+221
@@ -0,0 +1,221 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Exception.Lifted
3+-- Copyright : (c) 2019 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+
9+module Streamly.Internal.Data.Stream.Exception.Lifted
10+ (
11+ after
12+ , bracket
13+ , bracket'
14+ , finally
15+
16+ -- For IsStream module
17+ , afterD
18+ , bracket3D
19+ )
20+where
21+
22+#include "inline.hs"
23+
24+import Control.Exception (SomeException, mask_)
25+import Control.Monad.Catch (MonadCatch)
26+import GHC.Exts (inline)
27+import Streamly.Internal.Control.Concurrent
28+ (MonadRunInIO, MonadAsync, withRunInIO)
29+import Streamly.Internal.Data.Stream.Type (Stream, fromStreamD, toStreamD)
30+import Streamly.Internal.Data.IOFinalizer.Lifted
31+ (newIOFinalizer, runIOFinalizer, clearingIOFinalizer)
32+
33+import qualified Control.Monad.Catch as MC
34+import qualified Streamly.Internal.Data.Stream.StreamD as D
35+
36+import Streamly.Internal.Data.Stream.StreamD.Type hiding (Stream)
37+
38+-- $setup
39+-- >>> :m
40+-- >>> import qualified Streamly.Internal.Data.Stream.Exception.Lifted as Stream
41+
42+-- XXX Implement in terms of the corresponding IO operation (gbracketIO).
43+
44+data GbracketIOState s1 s2 v wref
45+ = GBracketIOInit
46+ | GBracketIONormal s1 v wref
47+ | GBracketIOException s2
48+
49+{-# INLINE_NORMAL gbracket #-}
50+gbracket
51+ :: MonadRunInIO m
52+ => m c -- ^ before
53+ -> (c -> m d1) -- ^ on normal stop
54+ -> (c -> e -> D.Stream m b -> m (D.Stream m b)) -- ^ on exception
55+ -> (c -> m d2) -- ^ on GC without normal stop or exception
56+ -> (forall s. m s -> m (Either e s)) -- ^ try (exception handling)
57+ -> (c -> D.Stream m b) -- ^ stream generator
58+ -> D.Stream m b
59+gbracket bef aft onExc onGC ftry action =
60+ D.Stream step GBracketIOInit
61+
62+ where
63+
64+ -- If the stream is never evaluated the "aft" action will never be
65+ -- called. For that to occur we will need the user of this API to pass a
66+ -- weak pointer to us.
67+ {-# INLINE_LATE step #-}
68+ step _ GBracketIOInit = do
69+ -- We mask asynchronous exceptions to make the execution
70+ -- of 'bef' and the registration of 'aft' atomic.
71+ -- A similar thing is done in the resourcet package: https://git.io/JvKV3
72+ -- Tutorial: https://markkarpov.com/tutorial/exceptions.html
73+ (r, ref) <- withRunInIO $ \run -> mask_ $ run $ do
74+ r <- bef
75+ ref <- newIOFinalizer (onGC r)
76+ return (r, ref)
77+ return $ Skip $ GBracketIONormal (action r) r ref
78+
79+ step gst (GBracketIONormal (UnStream step1 st) v ref) = do
80+ res <- ftry $ step1 gst st
81+ case res of
82+ Right r -> case r of
83+ Yield x s ->
84+ return $ Yield x (GBracketIONormal (D.Stream step1 s) v ref)
85+ Skip s ->
86+ return $ Skip (GBracketIONormal (D.Stream step1 s) v ref)
87+ Stop ->
88+ clearingIOFinalizer ref (aft v) >> return Stop
89+ -- XXX Do not handle async exceptions, just rethrow them.
90+ Left e -> do
91+ -- Clearing of finalizer and running of exception handler must
92+ -- be atomic wrt async exceptions. Otherwise if we have cleared
93+ -- the finalizer and have not run the exception handler then we
94+ -- may leak the resource.
95+ stream <-
96+ clearingIOFinalizer ref (onExc v e (UnStream step1 st))
97+ return $ Skip (GBracketIOException stream)
98+ step gst (GBracketIOException (UnStream step1 st)) = do
99+ res <- step1 gst st
100+ case res of
101+ Yield x s ->
102+ return $ Yield x (GBracketIOException (D.Stream step1 s))
103+ Skip s -> return $ Skip (GBracketIOException (D.Stream step1 s))
104+ Stop -> return Stop
105+
106+{-# INLINE_NORMAL bracket3D #-}
107+bracket3D :: (MonadAsync m, MonadCatch m) =>
108+ m b
109+ -> (b -> m c)
110+ -> (b -> m d)
111+ -> (b -> m e)
112+ -> (b -> D.Stream m a)
113+ -> D.Stream m a
114+bracket3D bef aft onExc onGC =
115+ gbracket
116+ bef
117+ aft
118+ (\a (e :: SomeException) _ -> onExc a >> return (nilM (MC.throwM e)))
119+ onGC
120+ (inline MC.try)
121+
122+-- For a use case of this see the "streamly-process" package. It needs to kill
123+-- the process in case of exception or garbage collection, but waits for the
124+-- process to terminate in normal cases.
125+
126+-- | Like 'bracket' but can use separate cleanup actions depending on the mode
127+-- of termination. @bracket' before onStop onGC onException action@ runs
128+-- @action@ using the result of @before@. If the stream stops, @onStop@ action
129+-- is executed, if the stream is abandoned @onGC@ is executed, if the stream
130+-- encounters an exception @onException@ is executed.
131+--
132+-- /Pre-release/
133+{-# INLINE bracket' #-}
134+bracket' :: (MonadRunInIO m, MonadCatch m)
135+ => m b
136+ -> (b -> m c)
137+ -> (b -> m d)
138+ -> (b -> m e)
139+ -> (b -> Stream m a)
140+ -> Stream m a
141+bracket' bef aft gc exc bet = fromStreamD $
142+ bracket3D bef aft exc gc (toStreamD . bet)
143+
144+-- | Run the alloc action @m b@ with async exceptions disabled but keeping
145+-- blocking operations interruptible (see 'Control.Exception.mask'). Use the
146+-- output @b@ as input to @b -> Stream m a@ to generate an output stream.
147+--
148+-- @b@ is usually a resource under the state of monad @m@, e.g. a file
149+-- handle, that requires a cleanup after use. The cleanup action @b -> m c@,
150+-- runs whenever the stream ends normally, due to a sync or async exception or
151+-- if it gets garbage collected after a partial lazy evaluation.
152+--
153+-- 'bracket' only guarantees that the cleanup action runs, and it runs with
154+-- async exceptions enabled. The action must ensure that it can successfully
155+-- cleanup the resource in the face of sync or async exceptions.
156+--
157+-- When the stream ends normally or on a sync exception, cleanup action runs
158+-- immediately in the current thread context, whereas in other cases it runs in
159+-- the GC context, therefore, cleanup may be delayed until the GC gets to run.
160+--
161+-- /See also: 'bracket_'/
162+--
163+-- /Inhibits stream fusion/
164+--
165+-- @since 0.9.0
166+{-# INLINE bracket #-}
167+bracket :: (MonadRunInIO m, MonadCatch m)
168+ => m b -> (b -> m c) -> (b -> Stream m a) -> Stream m a
169+bracket bef aft = bracket' bef aft aft aft
170+
171+-- | Run the action @m b@ whenever the stream @Stream m a@ stops normally,
172+-- aborts due to an exception or if it is garbage collected after a partial
173+-- lazy evaluation.
174+--
175+-- The semantics of running the action @m b@ are similar to the cleanup action
176+-- semantics described in 'bracket'.
177+--
178+-- >>> finally action xs = Stream.bracket (return ()) (const action) (const xs)
179+--
180+-- /See also 'finally_'/
181+--
182+-- /Inhibits stream fusion/
183+--
184+-- @since 0.9.0
185+{-# INLINE finally #-}
186+finally :: (MonadRunInIO m, MonadCatch m) =>
187+ m b -> Stream m a -> Stream m a
188+finally action xs = bracket (return ()) (const action) (const xs)
189+
190+{-# INLINE_NORMAL afterD #-}
191+afterD :: MonadRunInIO m
192+ => m b -> D.Stream m a -> D.Stream m a
193+afterD action (D.Stream step state) = D.Stream step' Nothing
194+
195+ where
196+
197+ {-# INLINE_LATE step' #-}
198+ step' _ Nothing = do
199+ ref <- newIOFinalizer action
200+ return $ Skip $ Just (state, ref)
201+ step' gst (Just (st, ref)) = do
202+ res <- step gst st
203+ case res of
204+ Yield x s -> return $ Yield x (Just (s, ref))
205+ Skip s -> return $ Skip (Just (s, ref))
206+ Stop -> do
207+ runIOFinalizer ref
208+ return Stop
209+
210+-- | Run the action @m b@ whenever the stream @Stream m a@ stops normally, or
211+-- if it is garbage collected after a partial lazy evaluation.
212+--
213+-- The semantics of the action @m b@ are similar to the semantics of cleanup
214+-- action in 'bracket'.
215+--
216+-- /See also 'after_'/
217+--
218+-- @since 0.9.0
219+{-# INLINE after #-}
220+after :: MonadRunInIO m => m b -> Stream m a -> Stream m a
221+after action xs = fromStreamD $ afterD action $ toStreamD xs
src/Streamly/Internal/FileSystem/Event/Windows.hs+2−3
@@ -90,7 +90,6 @@ module Streamly.Internal.FileSystem.Event.Windows
9090 )
9191where
9292
93−import Control.Monad.IO.Class (MonadIO(liftIO))
9493import Data.Bits ((.|.), (.&.), complement)
9594import Data.Char (ord)
9695import Data.Functor.Identity (runIdentity)
@@ -479,12 +478,12 @@ closePathHandleStream =
479478--
480479watchWith :: (Config -> Config) -> NonEmpty (Array Word8) -> Stream IO Event
481480watchWith f paths =
482− S.bracket before after (Concur.concatMapWith eager eventStreamAggr)
481+ S.bracketIO before after (Concur.concatMapWith eager eventStreamAggr)
483482
484483 where
485484
486485 before = return $ pathsToHandles (utf8ToStringList paths) $ f defaultConfig
487− after = liftIO . closePathHandleStream
486+ after = closePathHandleStream
488487
489488-- | Same as 'watchWith' using 'defaultConfig' and recursive mode.
490489--
streamly.cabal+1
@@ -366,6 +366,7 @@ library
366366
367367 -- Exposed modules
368368 , Streamly.Data.Stream.Concurrent
369+ , Streamly.Data.Stream.Exception
369370
370371 -- Network/IO
371372 , Streamly.Network.Socket