Help needed interrupting accepting a network connection
I'd like to write a server accepting incoming network connections that can be gracefully shutdown. When the server is asked to shutdown, it should stop accepting new connections, finish processing any current connections, and then terminate. Clients can retry if they attempt to make a connection and the connection is refused. This allows the server to restart seamlessly: any existing connections are not interrupted, and clients will see at most a pause while the server restarts. I am using the model from Simon Marlow's Haskell Web Server (as updated by Björn Bringert and available at http://www.cs.chalmers.se/~bringert/darcs/hws/): spawning a lightweight Haskell child thread for each client connection. In Control.Exception, I see that operations such as "accept" are interruptible by exceptions thrown to the thread, so I can interrupt an accept with a dynamic exception.
-- create a datatype to use to interrupt the accept data ExitGracefully = ExitGracefully deriving Typeable
I want to control when I'm paying attention to the ExitGracefully exception. I don't want to get the exception when I'm in the middle of updating a data structure, just in a few controlled points such as when I'm in an accept. Reading further in Control.Exception, I see that I can use "block" to put off receiving the exception generally, but accept is an interruptible operation so I don't need to do anything more to get the exception inside of the accept.
block ( ... result <- catchDyn (do (clientSocket, addr) <- accept sock return $ Just clientSocket) (\ (e :: ExitGracefully) -> return Nothing)
Typing the exception "e" as an "ExitGracefully" tells catchDyn that I only need to catch exceptions of that type. If the thread has been thrown a ExitGracefully, "result" will be Nothing, but if accept returned with a client connection, "result" will be Just the clientSocket.
case result of
Nothing -> do { putStrLn "accept loop exiting"; putMVar acceptLoopDone () }
Just clientSocket ->
So far so good. I also want to keep track of when the threads spawned to handle the client connections are finished, so I use the code from the "Terminating the program" section of the Control.Concurrent documentation to keep a list of MVar's indicating when the child threads are done:
childDone <- newEmptyMVar childDoneList <- takeMVar childrenDone putMVar childrenDone (childDone : childDoneList)
then I fork a child thread to handle the connection:
clientHandle <- socketToHandle clientSocket ReadWriteMode forkIO $ handleConnection childDone clientHandle
"handleConnection" runs inside the child thread, communicating with the client. When done, it closes the clientHandle, and does an "putMVar childDone ()" to say that it done. Except that, whoops, the "takeMVar" in the accept thread code which updates the childrenDone" MVar is also interruptible. So now I'm getting an interruption right where I don't want it, when I'm updating my data structure. Only the accept thread is thrown the ExitGracefully exception, so one thought I had was that I could move those three lines which update the childrenDone MVar into the child thread. But this introduces a race condition: as the server was shutting down, it could look at the childrenDone list and see that it was empty, before the child thread had a chance to start running and update the data structure to say that there was another child that needed to be waited for. Or, updating the childrenDone MVar could be done in its own thread, which again would protect it from the ExitGracefully exception... except that how would the accept thread wait for that thread... except by using an MVar? Oops, again. Any ideas? For reference sake here's the complete implementation. (This code is in the public domain... in case it would be useful to anyone else). Thank you, Cat
-- A ConnectionHandler is a function which handles an incoming -- client connection. The handler is run in its own thread, and is -- passed a handle to the client socket. The handler does whatever -- communication it wants to do with the client, and when it returns, -- the client socket handle is closed and the thread terminates. -- A list of active handlers is kept, and the client connection is -- also marked as finished when the handler returns.
type ConnectionHandler = Handle -> IO ()
example_connection_handler :: ConnectionHandler
example_connection_handler handle = do hPutStrLn handle "Hello." hPutStrLn handle "Goodbye."
type ChildrenDone = MVar [MVar ()]
data ExitGracefully = ExitGracefully deriving Typeable
waitForChildren :: ChildrenDone -> IO ()
waitForChildren childrenDone = do cs <- takeMVar childrenDone case cs of [] -> return () m:ms -> do putMVar childrenDone ms takeMVar m waitForChildren childrenDone
shutdownServer :: MVar () -> ChildrenDone -> ThreadId -> IO ()
shutdownServer acceptLoopDone childrenDone acceptThreadId = do throwDynTo acceptThreadId ExitGracefully takeMVar acceptLoopDone waitForChildren childrenDone return ()
acceptConnections :: MVar () -> ChildrenDone -> ConnectionHandler -> Socket -> IO ()
acceptConnections acceptLoopDone childrenDone connectionHandler sock = do block (acceptConnections' acceptLoopDone childrenDone connectionHandler sock)
acceptConnections' acceptLoopDone childrenDone connectionHandler sock = do
result <- catchDyn (do (clientSocket, addr) <- accept sock return $ Just clientSocket) (\ (e :: ExitGracefully) -> return Nothing)
case result of
Nothing -> do { putStrLn "accept loop exiting"; putMVar acceptLoopDone () }
Just clientSocket -> do clientHandle <- socketToHandle clientSocket ReadWriteMode childDone <- newEmptyMVar childDoneList <- takeMVar childrenDone putMVar childrenDone (childDone : childDoneList) forkIO $ handleConnection childDone connectionHandler clientHandle acceptConnections' acceptLoopDone childrenDone connectionHandler sock
handleConnection childDone connectionHandler clientHandle = do Exception.catch (connectionHandler clientHandle `finally` do { hClose clientHandle; putMVar childDone () })
-- TODO we'll want to do something better when -- connectionHandler throws an exception, but -- for now we'll at least display the exception. (\e -> do { putStrLn $ show e; return () })
Hi, I have taken a crack at this. The best thing would be not to use the asynchronous exceptions to signal the thread that calls accept. And use STM more, since the exception semantics are much easier to get right. But a few minor changes gets closer to what you want. First, the main problem you claim to run into is
Except that, whoops, the "takeMVar" in the accept thread code which updates the childrenDone" MVar is also interruptible. So now I'm getting an interruption right where I don't want it, when I'm updating my data structure.
Short version: There is no problem because it will not become interruptible. Long version: The takeMVar unblocks exceptions only if it must stop and wait for the MVar. The MVar is only taken by this command/thread and during graceful shutdown after this thread is dead. So this MVar should never be in contention (and in theory does not *need* to be a locked MVar, and an IORef would do). See http://citeseer.ist.psu.edu/415348.html for why I think takeMVar only allow exceptions if the MVar is unavailable. The biggest change is ensuring the accepting thread puts to acceptLoopDone by using finally. Many things might kill that thread; it is best to ensure it lets the main thread know that it is dead. More subtlety, I added "unblock (return ())" before accept. This makes it look for the asynchronous exception even when an incoming connection would be immediately available. Otherwise a busy server would never notice the exception! As a style point: there is an ugly moment between takeMVar and putMVar in which you state is inconsistent (being inside block makes it safe though). So I changed this to modifyMVar_ which is better practice.
import Control.Concurrent import Control.Concurrent.MVar import Control.Exception as Exception import Network.Socket import Data.Typeable import System.IO
-- A ConnectionHandler is a function which handles an incoming -- client connection. The handler is run in its own thread, and is -- passed a handle to the client socket. The handler does whatever -- communication it wants to do with the client, and when it returns, -- the client socket handle is closed and the thread terminates. -- A list of active handlers is kept, and the client connection is -- also marked as finished when the handler returns.
type ConnectionHandler = Handle -> IO ()
example_connection_handler :: ConnectionHandler
example_connection_handler handle = do hPutStrLn handle "Hello." hPutStrLn handle "Goodbye."
type ChildrenDone = MVar [MVar ()]
data ExitGracefully = ExitGracefully deriving Typeable
waitForChildren :: ChildrenDone -> IO ()
waitForChildren childrenDone = do cs <- takeMVar childrenDone mapM_ takeMVar cs
shutdownServer :: MVar () -> ChildrenDone -> ThreadId -> IO ()
shutdownServer acceptLoopDone childrenDone acceptThreadId = do throwDynTo acceptThreadId ExitGracefully takeMVar acceptLoopDone -- There can be no more changes to childrenDone waitForChildren childrenDone return ()
acceptConnections :: MVar () -> ChildrenDone -> ConnectionHandler -> Socket -> IO ()
acceptConnections acceptLoopDone childrenDone connectionHandler sock = finially (acceptConnections' acceptLoopDone childrenDone connectionHandler sock) (putStrLn "accept loop exiting" >> putMVar acceptLoopDone () ) -- run last
-- This only looks for exceptions when "accept sock" is executed acceptConnections' acceptLoopDone childrenDone connectionHandler sock = block loop where loop = do unblock (return ()) -- safe point to be interrupted, so unblock (clientSocket, addr) <- accept sock -- may or may not unblock and wait clientHandle <- socketToHandle clientSocket ReadWriteMode childDone <- newEmptyMVar forkIO $ handleConnection childDone connectionHandler clientHandle modifyMVar_ childrenDone (return . (childDone:)) -- non-blocking atomic change to MVar loop
handleConnection childDone connectionHandler clientHandle = do Exception.catch (finially (connectionHandler clientHandle) (hClose clientHandle >> putMVar childDone () )
-- TODO we'll want to do something better when -- connectionHandler throws an exception, but -- for now we'll at least display the exception. (\e -> do { putStrLn $ show e; return () })
On 12/2/06, Chris Kuklewicz <haskell@list.mightyreason.com> wrote:
Hi, I have taken a crack at this. The best thing would be not to use the asynchronous exceptions to signal the thread that calls accept.
I'd certainly be most happy not to use asynchronous exceptions as the signalling mechanism, but how would you break out of the accept, except by receiving an asynchronous exception?
But a few minor changes gets closer to what you want. First, the main problem you claim to run into is
Except that, whoops, the "takeMVar" in the accept thread code which updates the childrenDone" MVar is also interruptible. So now I'm getting an interruption right where I don't want it, when I'm updating my data structure.
Short version: There is no problem because it will not become interruptible. Long version: The takeMVar unblocks exceptions only if it must stop and wait for the MVar. The MVar is only taken by this command/thread and during graceful shutdown after this thread is dead. So this MVar should never be in contention (and in theory does not *need* to be a locked MVar, and an IORef would do).
Gosh, I think you're right.
Cat Dancer wrote:
On 12/2/06, Chris Kuklewicz <haskell@list.mightyreason.com> wrote:
Hi, I have taken a crack at this. The best thing would be not to use the asynchronous exceptions to signal the thread that calls accept.
I'd certainly be most happy not to use asynchronous exceptions as the signalling mechanism, but how would you break out of the accept, except by receiving an asynchronous exception?
Short Version: You trigger a graceful exit using a TVar... ...and then you use killThread to break out of accept. Long Version: {- The main accepting thread spawns this a slave thread to run accept and stuffs the result into a TMVar. The main loop then atomically checks the TVar used for graceful shutdown and the TMVar. These two checks are combined by `orElse` which gives the semantics one wants: on each loop either the TVar has been set to True or the the slave thread has accepted a client into the TMVar. There is still the possibility that a busy server could accept a connection from the last client and put it in the TMVar where the main loop will miss it when it exits. This is handled by the finally action which waits for the slave thread to be well and truly dead and then looks for that last client in the TMVar. No uses of block or unblock are required. -} -- Example using STM and orElse to compose a solution import Control.Concurrent import Control.Exception import Control.Concurrent.STM import Network import System.IO runExampleFor socket seconds = do tv <- newTVarIO False -- Set to True to indicate graceful exit requested sInfo <- startServer socket tv threadDelay (1000*1000*seconds) shutdownServer tv sInfo startServer socket tv = do childrenList <- newMVar [] tInfo <- fork (acceptUntil socket exampleReceiver childrenList (retry'until'true tv)) return (tInfo,childrenList) -- Capture idiom of notifying a new MVar when a thread is finished fork todo = do doneMVar <- newEmptyMVar tid <- forkIO $ finally todo (putMVar doneMVar ()) return (doneMVar,tid) acceptUntil socket receiver childrenList checker = do chan <- newEmptyTMVarIO (mv,tid) <- fork (forever (accept socket >>= syncTMVar chan)) let loop = do result <- atomically (fmap Left checker `orElse` fmap Right (takeTMVar chan)) case result of Left _ -> return () Right client -> spawn client >> loop spawn client@(handle,_,_) = do cInfo <- fork (finally (receiver client) (hClose handle)) modifyMVar_ childrenList (return . (cInfo:)) end = do killThread tid takeMVar mv maybeClient <- atomically (tryTakeTMVar chan) maybe (return ()) spawn maybeClient finally (handle (\e -> throwTo tid e >> throw e) loop) end forever x = x >> forever x -- Pass item to another thread and wait for pickup syncTMVar tmv item = do atomically (putTMVar tmv item) atomically (do empty <- isEmptyTMVar tmv if empty then return () else retry) retry'until'true tv = do val <- readTVar tv if val then return () else retry exampleReceiver (handle,_,_) = do hPutStrLn handle "Hello." hPutStrLn handle "Goodbye." shutdownServer tv ((acceptLoopDone,_),childrenList) = do atomically (writeTVar tv True) readMVar acceptLoopDone withMVar childrenList (mapM_ (readMVar . fst))
On 2 dec 2006, at 22.13, Cat Dancer wrote:
I'd like to write a server accepting incoming network connections that can be gracefully shutdown.
When the server is asked to shutdown, it should stop accepting new connections, finish processing any current connections, and then terminate.
Clients can retry if they attempt to make a connection and the connection is refused. This allows the server to restart seamlessly: any existing connections are not interrupted, and clients will see at most a pause while the server restarts.
I am using the model from Simon Marlow's Haskell Web Server (as updated by Björn Bringert and available at http://www.cs.chalmers.se/~bringert/darcs/hws/): spawning a lightweight Haskell child thread for each client connection. ...
I'd just like to add that the main repository for the conservatively updated HWS is http://darcs.haskell.org/hws/ There is also a more aggressively updated version at http:// www.cs.chalmers.se/~bringert/darcs/hws-cgi/ It supports CGI programs, has a module system for extensibility (no dynamic module loading though), more flexible logging, support for listening on multiple sockets, and many other changes. Contributions to either of these are very welcome. /Björn
participants (3)
-
Bjorn Bringert -
Cat Dancer -
Chris Kuklewicz