I've been digging into async exceptions in haskell, and getting more
and more concerned. In particular, bracket
seems to be often used in ways
that are not async exception safe. I've found multiple libraries with problems.
Here's an example:
withTempFile a = bracket setup cleanup a
where
setup = openTempFile "/tmp" "tmpfile"
cleanup (name, h) = do
hClose h
removeFile name
This looks reasonably good, it makes sure to clean up after itself even when the action throws an exception.
But, in fact that code can leave stale temp files lying around.
If the thread receives an async exception when hClose
is
running, it will be interrupted before the file is removed.
We normally think of bracket
as masking exceptions, but it
doesn't prevent async exceptions in all cases.
See Control.Exception on "interruptible operations",
which can receive async exceptions even when other exceptions are masked.
It's a bit surprising, but hClose
is such an interruptable operation,
because it flushes the write buffer. The only way to know is to
read the code.
It can be quite hard to determine if an operation is interruptable, since it can come down to whether it retries a STM transaction, or uses a MVar that is not always full. I've been auditing libraries and I often have to look at code several dependencies away, and even then may not be sure if a library has this problem.
process's withCreateProcess could fail to wait on the process, leaving a zombie. Might also leak file descriptors?
http-client's withResponse might fail to close a network connection. (If a MVar happened to be empty when it's called.)
Worth noting that there are plenty of examples of using http-client to eg, race downloading two urls and cancel the slower download. Which is just the kind of use of an async exception that could cause a problem.
persistent's withSqlPool and withSqlConn might fail to clean up, when used with persistent-postgresql. (If another thread is using the connection and so a MVar over in postgresql-simple is empty.)
concurrent-output has some locking code that is not async exception safe. (My library, so I've fixed part of it, and hope to fix the rest.)
So far, around half of the libraries I've looked at, that use bracket
or onException
or the like probably have this problem.
What can libraries do?
Document whether these things are async exception safe. Or perhaps there should be an expectation that "withFoo" always is, but if so the Haskell comminity has some work ahead of it.
Use
finally
. Good mostly in simple situations; more complicated things would be hard to write this way.hClose h `finally` removeFile name
Use
uninterruptibleMask
, but it's a big hammer and is often not the right tool for the job. If the operation takes a while to run, the program will not respond to ctrl-c during that time.May be better to run the actions in worker threads, to insulate them from receiving any async exceptions.
bracketInsulated :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c bracketInsulated a b = bracket (uninterruptibleMask $ \u -> async (u a) >>= u . wait) (\v -> uninterruptibleMask $ \u -> async (u (b v)) >>= u . wait)
(Note use of uninterruptibleMask here in case async itself does an interruptable operation. My first version got that wrong.. This is hard!)
My impression of the state of things now is that you should be very cautious
using race
or cancel
or withAsync
or the like, unless the thread is small
and easy to audit for these problems. Kind of a shame, since I had wanted to
be able to cancel a thread that is big and sprawling and uses all the
libraries mentioned above.
This work was sponsored by Jake Vosloo and Graham Spencer on Patreon.
@alex safe-exceptions and unliftio use uninterruptibleMask in its async safe bracket. Which is ok if the cleanup action is fast, but does risk the program not responding to ctrl-c if the cleanup takes a while for whatever reason.
As well as SIGINT, there's also the possibility that an async exception is thrown for some truely exceptional circumstance, like a segfault. Most code would do well to exit immediately on such an exception, not mask it.
I wonder if there's a way to make an uninterruptibleMask that masks only a specific async exception, eg the AsyncCancelled exception. Probably this would need ghc support, if it's possible at all.