Re: [Haskell] Help needed interrupting accepting a network connection
Cat Dancer wrote:
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.
Oh, OK, you're still using an asynchronous exception to break out of the accept (killThread throws a ThreadKilled asynchronous exception to the thread), but you're using STM to *signal* the graceful exit instead of using the asynchronous exception as the signalling mechanism. Nice.
Thanks. My ghc 6.6 (needed for newTVarIO) installation is broken for some reason, so I'll need to fix that tomorrow and then I'll be able to try your code.
Since newTVarIO is not in unsafePerformIO, you can replace it with "atomically (newTVar)"
(mv,tid) <- fork (forever (accept socket >>= syncTMVar chan))
It looks like to me you could get a connection from "accept" but then get a ThreadKilled exception before the "syncTMVar chan" executes, and then the connection would be left open and hanging until it was eventually garbage collected?
Sigh. I missed that one. Not bad to fix, just use block and split syncTMVar, putting the unblocked empty check before the accept. I also switched to using "cond", a kind of flipped "if". The new code: cond true false test = if test then true else false acceptUntil socket receiver childrenList checker = do chan <- newEmptyTMVarIO (mv,tid) <- fork . block . forever $ do unblock . atomically $ isEmptyTMVar chan >>= cond (return ()) retry client <- accept socket atomically (putTMVar chan client) 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 readMVar mv maybeClient <- atomically (tryTakeTMVar chan) maybe (return ()) spawn maybeClient finally (handle (\e -> throwTo tid e >> throw e) loop) end The new code makes sure chan is empty, and so we are sure the putTMVar chan will never have to wait so it will never unblock (I just wrote and ran short test to confirm this). I think this is fixed now.
I realized there is another problem, since my code holds onto the ThreadId's the thread data structures may or may not be getting garbage collected and for a long running server the list of children grows without bound. So I changed it to periodically clean out the finished child threads from the list of children. A simple counter IORef is used to avoid doing the cleanup on each new child. There are also a couple of other small style changes.
{-
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.
The list of child threads is cleaned periodically (currently every 10th child), which allows the garbage collected to remove the dead threads' structures.
-}
-- Example using STM and orElse to compose a solution import Control.Monad import Control.Concurrent import Control.Exception import Control.Concurrent.STM import Data.IORef import Network import System.IO
forever x = x >> forever x
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)
shutdownServer tv ((acceptLoopDone,_),childrenList) = do atomically (writeTVar tv True) readMVar acceptLoopDone withMVar childrenList (mapM_ (readMVar . fst))
-- 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)
cond true false test = if test then true else false
-- This is an asychronous exception safe way to use accept to get one -- client at a time and pass them to the parent thread via a TMVar. acceptInto socket chan = block . forever $ do unblock . atomically $ isEmptyTMVar chan >>= cond (return ()) retry client <- accept socket atomically (putTMVar chan client)
-- This demonstrates how to use acceptInto to spawn client thread -- running "receiver". It ends when checker commits instead of using -- retry. acceptUntil socket receiver childrenList checker = do counter <- newIORef (0::Int) -- who cares if it rolls over? chan <- atomically (newEmptyTMVar) (mv,tid) <- fork (acceptInto socket chan) let loop = atomically (fmap Left checker `orElse` fmap Right (takeTMVar chan)) >>= either (const (return ())) (\client -> spawn client >> loop) spawn client@(handle,_,_) = do cInfo <- fork (finally (receiver client) (hClose handle)) count <- readIORef counter writeIORef counter $! (succ count) modifyMVar_ childrenList $ \kids -> fmap (cInfo:) $ if count `mod` 10 == 0 -- 10 is arbitrary frequency for cleaning list then return kids else filterM (isEmptyMVar . fst) kids end = do killThread tid readMVar mv atomically (tryTakeTMVar chan) >>= maybe (return ()) spawn finally (handle (\e -> throwTo tid e >> throw e) loop) end
exampleReceiver (handle,_,_) = do hPutStrLn handle "Hello." hPutStrLn handle "Goodbye."
retry'until'true tv = (readTVar tv >>= cond (return ()) retry)
After more testing I found an ugly problem that a child could be killed before the finally installed the handler that calls (putMVar doneMVar ()) Thus I have added slightly more paranoid code to ensure that the child is running before exposing the (T)MVar/ThreadId to the rest of the application. That this was needed is is really really annoying, and does not *really* fix the problem but just minimize it. Change fork to:
fork todo = block $ do doneVar <- atomically (newEmptyTMVar) let putStarted = atomically (putTMVar doneVar False) putStopped = atomically (tryTakeTMVar doneVar >> putTMVar doneVar True) tid <- forkIO $ block $ (finally (putStarted >> unblock todo) putStopped) yield atomically $ do value <- takeTMVar doneVar when value (putTMVar doneVar True) return (doneVar,tid)
and in the rest of the code change a few readMVar to readTMVar and add atomically. The doneVar is in 3 states: empty meaning child has not started yet False meaning child has definitely started empty meaning meaning child is still running True meaning child has definitely stopped The first two of those states should only be seen inside the fork function. When the fork function is finished only the second two states should be seen. -- Chris
participants (1)
-
Chris Kuklewicz