Catching (async) exceptions

In my attempts at improving how we use Haskell at work I’m trying to make sure k8s doesn’t silently kill our services when running out of memory. So I’ve added +RTS -MxM, where xM is a reasonable amount less than the available memory. That lead to some services throwing HeapOverflow exceptions. We have some rules about the format of our logs and the default exception handler (defaultHandler, defined in uncaughtExceptionHandler) doesn’t come close. So, I thought I’d play around a little to see what my options are, but I very quickly got confused. I started looking at forkIO.

It seems that the default exception handler (defaultHandler, defined in uncaughtExceptionHandler) doesn’t handle async exceptions at all. Even though the documentation of forkIO says it does. Well, it does say it “passes all other exceptions to the uncaught exception handler”, I assumed that’d be the one I can set using setUncaughtExceptionHandler, but setting it has no effect.

I’ve found that this code (mostly) works

module Main where

import Control.Concurrent (forkFinally, forkIO, myThreadId, threadDelay)
import Control.Exception (SomeException, handle, throwIO)
import GHC.Conc (setUncaughtExceptionHandler)

main :: IO ()
main = do
    handle myExceptionHandler $ do
        putStrLn "13: trigger a sync exception in a forked thread - with my explicit exception handler"
        thrid <- forkIO threadFunc
        putStrLn $ "forked thread ID: " <> show thrid
        threadDelay 2_500_000
    putStrLn "time to exit"

threadFunc :: IO ()
threadFunc = do
    putStrLn "running thread"
    let f x = 1 + f x
    print $ f 1

myExceptionHandler :: SomeException -> IO ()
myExceptionHandler e = do
    myId <- myThreadId
    putStrLn $ str myId
  where
    str tid = "caught exception on " <> show tid <> ": " <> show e

I compiled it using ghc --make -threaded -rtsopts -o exp13 exp13.hs.

I write “mostly” because most times I get the expected output when running it

> ./exp13 +RTS -M50M; print $?
13: trigger a sync exception in a forked thread - with my explicit exception handler
running thread
forked thread ID: ThreadId 5
caught exception on ThreadId 4: heap overflow
time to exit
0

But then, every now and then I get this

./exp13 +RTS -M50M; print $?
13: trigger a sync exception in a forked thread - with my explicit exception handler
running thread
forked thread ID: ThreadId 5
caught exception on ThreadId 4: heap overflow
exp13: Heap exhausted;
exp13: Current maximum heap size is 52428800 bytes (50 MB).
exp13: Use `+RTS -M<size>' to increase it.
251

It looks like both my exception handler and the default where triggered. Surely that should never happen, right?

ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.12.3

I just realised that I ought to run with -N too, and now I can’t seem to reproduce the behaviour from above.

I did see some even stranger behaviour when I’d forgotten the -threaded flag, So is that the cause, I must use -threaded when compiling and at least 2 capabilities, -N2, when running it to guarantee the expected behaviour?