What you need to know about bracket
Published on ; updated on
The bracket
function in Haskell can be used to release
resources reliably:
bracket :: IO a -- ^ computation to run first ("acquire resource")
-> (a -> IO b) -- ^ computation to run last ("release resource")
-> (a -> IO c) -- ^ computation to run in-between
-> IO c -- returns the value from the in-between computation
There are a couple of things you should know when using
bracket
, lest you be surprised by the results.
First, in some cases the release action may not be called. Or it may be called but not given a chance to complete.
Second, the acquire and release action are run with async exception masked, which may not be what you want.
Release guarantee
It is useful to distinguish two different kinds of resources, let’s call them “internal” and “external”.
An example of an internal resource is an MVar
that you
take (acquire) and put back (release):
withMVar :: MVar a -> (a -> IO b) -> IO b
=
withMVar m io bracket (takeMVar m) (putMVar m) io
An example of an external resource is a directory that you create (acquire) and remove (release):
withFoo :: IO a -> IO a
=
withFoo io "foo") (removeDirectoryRecursive "foo") io bracket_ (createDirectory
The release of an internal resource is noticable only within your program, and it only matters as long as your program keeps running. If the program exits, it’s fine not to release an internal resource. An external resource, on the other hand, always needs to be released.
It is impossible to guarantee that external resources are always
released. The power may go off, or your program may be terminated with
SIGKILL
, and there won’t be time to clean up.
Thus, we would like a guarantee that:
- internal resources are released if the program keeps running, to avoid memory leaks and deadlocks
- external resources are released, assuming the program keeps running or is terminated in a “civil” way
Now let’s see why these promises may be broken and what to do about it.
bracket
in non-main
threads
The main thread is the Haskell thread that executes your
main
function.
When the main thread finishes, the program finishes, too, without sending exceptions to other threads and giving them any chance to clean up.
Therefore, if your bracket
is executed in some other
thread, its release action may not run when the program terminates.
This limitation is especially acute in library code, where you don’t know whether you’re running in the main thread or have any way to tie to it.
To avoid this problem, you need to manage your threads. Keep a list
of them (or weak pointers to them, see mkWeakThreadId
), and
give them a heads-up (in the form of an asynchronous exception) before
you exit from main
.
This problem only affects external resources.
asynchronous exceptions
Even if the release action is called, it may not complete if it is interrupted by an asynchronous exception.
A good example is the withTempDirectory
function defined
in the package temporary
:
=
withTempDirectory targetDir template
Exception.bracket
(liftIO (createTempDirectory targetDir template)). ignoringIOErrors . removeDirectoryRecursive) (liftIO
Bit Connor describes the issue in detail:
This function uses
bracket
which splits it up into three stages:
- “acquire” (create the directory)
- “in-between” (user action)
- “release” (recursively delete the directory)
Consider the following scenario:
- Stage 1 (“acquire”) completes successfully.
- Stage 2 (“user action”) places many files inside the temporary directory and completes successfully.
- Stage 3 begins: There are many files inside the temporary directory, and they are deleted one by one. But before they have all been deleted, an async exception occurs. Even though we are currently in a state of “masked” async exceptions (thanks to bracket), the individual file delete operations are “interruptible” and thus our mask will be pierced. The function will return before all of the temporary files have been deleted (and of course the temporary directory itself will also remain).
This is not good. “with-style” functions are expected to guarantee proper and complete clean up of their resources. And this is not just a theoretical issue: there is a significant likelihood that the problem can occur in practice, for example with a program that uses a temporary directory with many files and the user presses Ctrl-C.
This problem affects both internal and external resources if the release action includes any interruptible operations.
To prevent the interruption, wrap the release action in
uninterruptibleMask
.
signals
By default, only the SIGINT
signal is turned into a
Haskell exception by the GHC RTS.
If your process is terminated by any other signal, the release action won’t run.
To address this, turn signals into exceptions yourself:
import Control.Concurrent (mkWeakThreadId, myThreadId)
import Control.Exception (Exception(..), throwTo)
import Control.Monad (forM_)
import Data.Typeable (Typeable)
import System.Posix.Signals
import System.Mem.Weak (deRefWeak)
newtype SignalException = SignalException Signal
deriving (Show, Typeable)
instance Exception SignalException
installSignalHandlers :: IO ()
= do
installSignalHandlers <- myThreadId
main_thread_id <- mkWeakThreadId main_thread_id
weak_tid $ \sig ->
forM_ [ sigHUP, sigTERM, sigUSR1, sigUSR2, sigXCPU, sigXFSZ ] Catch $ send_exception weak_tid sig) Nothing
installHandler sig (where
= do
send_exception weak_tid sig <- deRefWeak weak_tid
m case m of
Nothing -> return ()
Just tid -> throwTo tid (toException $ SignalException sig)
= do
main
installSignalHandlers...
For the rationale behind this specific collection of signals (which is far from exhaustive), see this pull request by Aleksey Khudyakov.
This problem only affects external resources. It was pointed out to me by Jon Boulle and Mandeep Gill.
References:
Inadvertent mask
You could try to write something like
= do
main $ \_ -> do
bracket (forkIO myComputation) killThread ...
The idea here is that if main
exits, for whatever
reason, you’ll send an exception to the thread you forked, and give it a
chance to clean up.
First, this isn’t going to help much because main
will
exit right after killThread
, probably right in the middle
of myComputation
’s cleanup process. Some kind of
synchronisation should be introduced to address this properly. (The
price is that your program may not exit promptly when you interrupt it
with Ctrl-C.)
There’s another, more subtle issue with the code above. Let’s look at
the definition of bracket
:
=
bracket before after thing $ \restore -> do
mask <- before
a <- restore (thing a) `onException` after a
r <- after a
_ return r
As you see, the before
action is run in the masked
state. Forked threads inherit the masked state of their parents, so
myComputation
and all threads spawned by it will
unwittingly run in the masked state, unless they do
forkIOWithUnmask
.
In this simple case, you should just use withAsync
from
the async package. What about more complex ones?
If you do forking explicitly, then you can write
bracket
-like code yourself and restore the forked
computation. Here’s an example of synchronised cleanup:
= do
main <- atomically $ newTVar False
cleanupFlag $ \restore -> do
mask <- forkIO $ restore $ myComputation cleanupFlag
pid
restore restOfMain`finally` do
killThread pid-- wait until myComputation finishes its cleanup
-- and sets the flag to True
$ readTVar cleanupFlag >>= check atomically
(You could use an MVar
for synchronisation as well.)
And what if forking happens inside some library function that you
need to call? In that case, you may want to restore
that
whole function from the beginning.