Aug 24, 2017
This blog post was inspired by a recent Stack Overflow question. It also uses the Stack script interpreter for inline snippets if you want to play along at home. Don't forget to get Stack first.
The non trick case
Here's a non trick question: what do you think the output of
this series of shell commands is going to be?
If you guessed 42
, you're right. Our Haskell process uses exitWith
to exit the process with exit code 42
. Then echo $?
prints the last
exit code. All relatively straightforward (if you're familiar with
the shell).
Race condition
Alright, let's make it more fun with some concurrency
(concurrency makes everything more fun):
The output this time is nondeterministic. We don't know if the first thread (which exits with 41
) or the second thread (which exits with 42
) will exit first. I
tested this about 5 times on my machine, and got both
41
and 42
as outputs. So this isn't just theoretically nondetministic, it's practically
nondetministic.
Surprise! Warp
Alright, that's fine, probably nothing too terribly surprising.
Now let's throw the curve balls in. I'm going to write a web server
with Warp, and when someone requests /die
, I want the
server to go down. Here's the code. If you're not familiar with WAI
and Warp, just ignore the web bits and focus on the
exitWith
part:
Let's see what happens when we run it:
A few different weird things just happened:
When we made a request to
/die
, the serverapparently didn't die! We can see that from both the fact that the
next request succeeded, and the
fg
call.For some reason,
ExitFailure 43
is printed to theconsole. We can't tell here, but it's coming from the server
process.
And our HTTP response body contains the content
Something went wrong
, even though we didn't write that.
I would have expected the process to just die and get an empty
response. Why this surprising behavior instead?
Implementation of exitWith
To understand what's happening, let's look at a simplified
version of the implementation of the exitWith
function. Feel free to read the original as well.
I would have anticipated that this would, you know, actually
exit the process. Such a function does exist in Haskell. It's called exitImmediately
, it lives in the unix
package, and it calls out to the exit
C library function. But not exitWith
: it throws a runtime
exception.
There's a good reason for this exception-based behavior. It
allows cleanup code to run before the process just up and dies,
which would allow things like flushing file handles and gracefully
closing network connections. However, this can certainly result in
surprising behavior. We'll get back to the Warp case in a bit;
let's see something simpler first:
And the output is:
We've exited with code 0, a success! And our program continued
running after the call to exitWith
. That's because our tryAny
call intercepted the exception, converted it into a Left
value, and then our program succeeded in
printing out that value.
What's up with Warp?
Warp employs a pretty simple model for handling requests:
Grabs a listening port
Loops accepting connections on that port
For each connection, fork a new worker thread
Within each worker thread, Warp accepts a request, passes it to
the user-supplied application, takes the response, and sends it.
Additionally, Warp installs an exception handler in case the
application throws an exception. In that case, it will print the
exception to stderr and send a 500 Internal Server Error response
with the response body (wait for it) Something went wrong
.
So of course our initial attempt at killing our Warp application
failed: the exception was intercepted!
As an aside, if you really want to be able to exit from a Warp
application, you can see my answer on Stack Overflow, which I'm not going to detail here as it will
be a tangent to the main point.
Child threads in general
Alright, let's make another mistake (certainly my
specialty):
We're not intercepting the exception via a handler at all, and
thanks to our threadDelay
(which delays the parent
thread by one second), we have plenty of time for the child thread
to act before the parent exits on its own. Surely this will exit
with exit code 45, right?
Foiled again! We're running into something different now. In
GHC's runtime system, a process exits when the main thread exits.
If a child thread exits for any reason, the process keeps running.
If the main thread exits, even if there are still child threads
running, the process exits.
When we call forkIO
, a default exception handler is
installed on this new child thread. And that default exception
handler will simply print out the exception to stderr. That's the
Main.hs: ExitFailure 45
output we see.
As usual: async to the rescue
Where did we go wrong? By using the forkIO
function, of course! As I'm wont to say:
I think I've just told like the tenth person
this week "use the async library, you'll be much happier." Thanks
— Michael Snoyman (@snoyberg) July 2, 2017
The problem is that forkIO
installs a default
exception handler, instead of properly propagating exceptions
through our application. Fortunately, there's a great solution to
this, which we've already seen in this post: use the
concurrently
function from async
(or, in some cases, race
).
Any luck?
Woohoo! I've never been so happy to see a process exit with a
failure code before.
In contrast to forkIO
, the concurrently
and race
functions track the
exceptions occurring in their child threads and rethrow those
exceptions in the parent thread should anything go wrong. So
instead of exceptions disappearing into the aether, they tear down
our process with dignity.
If you're not familiar with the async library, check out the
tutorial I wrote on it, which focuses on using concurrently
and race
wherever possible.
Summary
Takeaways to remember:
exitWith
works by throwing exceptions, not directly killing the processA Haskell process dies when the main thread dies
Warp worker threads install an exception handler that generates 500 Internal Server Error responses
Use
concurrently
andrace
in place offorkIO
, and generally try to use theasync
library