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