Using `mask` and `mask_`

There is (a key) part of Stack’s code base that, simplifying, is like:

pure $ mask $ \restore -> do
  eres <- try $ restore $ actionDo ...
  atomically $ do ...
  restore loop

I understand from its Haddock documentation (base package, Control.Exception) that restore ‘turns off’ the mask (strictly “restores the prevailing masking state within the context of the masked computation”). So, I reason that the following code should be equivalent to that above:

pure $ do
  eres <- try $ actionDo ...
  mask_ $ atomically $ do ...
  loop

That is, the mask is effectively only applied to the action atomically $ do ....

My question is, does my reasoning seem sound, or am I missing something?

1 Like

The only difference I’m tracking between the two snippets is that the first snippet masks the call to try itself whereas the second snippet does not mask the call to try. The fact that the second snippet does not mask the call to try seems harmless, as there’s no cleanup action to speak of when using try.

Your reasoning seems sound to me!

1 Like

This is a bit subtle, but I think the first snippet has an important property that the second does not. The issue is that an asynchronous exception might be thrown immediately after try returns and before the next function call.

In the first snippet, masking is enabled at that point, so we are guaranteed that the atomically block will execute. Whereas in the second snippet, masking is not enabled, so it is possible for try to return but an exception to occur that prevents execution of the atomically block.

This article by Michael Snoyman is a helpful guide to the wild world of asynchronous exceptions:

3 Likes

Many thanks to you, and to @snoyberg, for the April 2018 article/video/slides that you identified, which explains your subtle point. That also led me to identify a June 2016 article on asynchronous exceptions and STM, which helped me understand why masking (in the Stack code) might be needed at all (as Stack does not use throwTo expressly).

So, to ensure that the actionDo ... action and the atomically ... action are not separated by the runtime checking, inbetween, if there has been an asynchronous exception, I could use:

pure $ do
  mask $ \restore -> do
    eres <- try $ restore $ actionDo ...
    atomically $ do ...
  loop

and that would also comply with Michael Snoyman’s adage: “minimize the amount of time you spend in a masked state”.

1 Like