Roman Cheplyaka

Beware of bracket

July 30, 2014

Many Haskellers know and love the bracket function:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

In our volatile world, bracket provides a safe haven of certainty: the resource will be released, no matter what. However, there’s a catch.

Unfulfilled promise

Is the resource you’re trying to release something internal to your code, or is observable outside?

In the former case, you’re fine. An example would be taking an MVar and putting it back afterwards:

withMVar :: MVar a -> (a -> IO b) -> IO b
withMVar m io =
  bracket (takeMVar m) (putMVar m) io

Now consider the case when the resource is something tangible — let’s say, a directory.

withFoo :: IO a -> IO a
withFoo io =
  bracket_ (createDirectory "foo") (removeDirectoryRecursive "foo") io

You might think that there’s no way for foo to remain in the filesystem after the program is done, barring OS-level issues.

In reality, you only get this guarantee if withFoo is executed in the main Haskell thread (i.e. the thread that executes main). When the main thread finishes, the program finishes, too, without sending exceptions to other threads and giving them any chance to finalize.

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.

Inadvertent mask

You could try to write something like

main = do
  bracket (forkIO myComputation) killThread $ \_ -> do

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 =
  mask $ \restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- 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:

main = do
  cleanupFlag <- atomically $ newTVar False
  mask $ \restore -> do
    pid <- forkIO $ restore $ myComputation cleanupFlag
    restore restOfMain
      `finally` do
        killThread pid
        -- wait until myComputation finishes its cleanup
        -- and sets the flag to True
        atomically $ readTVar cleanupFlag >>= check 

(You could use an MVar for synchronisation just 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.

Interrupted release action

This section was added on 2016-10-28.

There is another reason why the release action may not complete: it may be interrupted by an asynchronous exception.

A good example is the withTempDirectory function defined in the package temporary:

withTempDirectory targetDir template =
    (liftIO (createTempDirectory targetDir template))
    (liftIO . ignoringIOErrors . removeDirectoryRecursive)

Bit Connor describes the issue in detail:

This function uses bracket which splits it up into three stages:

  1. “acquire” (create the directory)
  2. “in-between” (user action)
  3. “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.

To prevent the interruption, wrap the release action in uninterruptibleMask.