streamly icon indicating copy to clipboard operation
streamly copied to clipboard

Efficient error handling in streams

Open harendra-kumar opened this issue 5 years ago • 1 comments

The onException combinator is essentially implemented as:

    step' gst st = do
        res <- step gst st `MC.onException` action
        case res of
            Yield x s -> return $ Yield x s
            Skip s    -> return $ Skip s
            Stop      -> return Stop

onException ultimately turns into a catch#, the step is wrapped into a catch# and this code cannot fuse and becomes very inefficient if we are doing exception handling on byte stream. Basically we have to check exceptions on each byte breaking fusion and allocating the constructor for each step. We can see this in the performance of exception combinators on a byte stream, these are the worst benchmarks in the FileSystem.Handle benchmark:

Benchmark                                                                                                            default(MiB)
-------------------------------------------------------------------------------------------------------------------- ------------
FileSystem.Handle/o-1-space/copy/read/group-ungroup/UA.unwords . UA.words (Array Char) (1/10)                             1576.53
FileSystem.Handle/o-1-space/copy/read/exceptions/S.finallyIO (1/10)                                                       1540.07
FileSystem.Handle/o-1-space/copy/read/exceptions/S.finally (1/10)                                                         1540.07
FileSystem.Handle/o-1-space/copy/read/exceptions/S.handle (1/10)                                                          1540.07
FileSystem.Handle/o-1-space/copy/read/exceptions/S.onException (1/10)                                                     1540.07
FileSystem.Handle/o-1-space/copy/fromToBytes/exceptions/S.bracketIO (1/10)                                                1379.72
FileSystem.Handle/o-1-space/copy/fromToBytes/exceptions/S.bracket (1/10)                                                  1379.72
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.bracketIO (1/10)                                                      1299.66
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.finallyIO (1/10)                                                      1299.66
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.handle (1/10)                                                         1299.66
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.bracket (1/10)                                                        1140.31
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.finally (1/10)                                                        1140.31
FileSystem.Handle/o-1-space/copy/read/exceptions/UF.onException (1/10)                                                    1140.31

Alternatively, we can have an Error constructor to propagate errors in the stream more efficiently. The error constructor will fuse nicely. At the points where we need to interface with IO and can encounter IO errors we can use catch# and turn that into an Error to propagate through the stream.

Note that we use an Error constructor in parsers for the same purpose, we can have the same in streams as well.

harendra-kumar avatar Jul 08 '20 21:07 harendra-kumar

By introducing Error we can propagate synchronous exceptions cheaply in a stream. This can help us remove try from bracket. However, in case of asynchronous exceptions resource release will still have to be done by a bracket outside the pipeline. Then why not use the monad level bracket in case of all exceptions (sync or async)? Therefore, we can just remove try from the bracket even now.

onException can handle both sync and async exceptions - in that case we can possibly use the Error constructor for efficient sync exception handling and let async exceptions be handled at the top level.

For the same reasons we can introduce the Error constructor in the Fold type as well. Note that this is not the same as the Parser's Error constructor - that one represents application level error (a parsing error) whereas this one represents extraneous errors or sync exceptions (any non-parsing error).

harendra-kumar avatar Jul 01 '25 17:07 harendra-kumar