10
votes

I have been attempting to debug a problem when using multiple MVars, however to no luck.

My code uses two MVars: one to store the servers current state, and another to pass network events to and from the client threads. However after connecting and disconnecting several times, the server stops sending data upon new clients connecting (presumably because the network events MVar is emptied for whatever reason) and eventually trips up with the error: *** Exception: thread blocked indefinitely in an MVar operation

I have concluded the following in trying to debug this issue over the last few days:

  1. The functions used to modify the MVar(s) wont throw exceptions
  2. The problem does not occur until a client either connects, or connects then disconnects
  3. The issue seems to occur randomly (sometimes several clients can connect then disconnect, other times it happens right away)

I have isolated the problem to three files:

  1. https://github.com/Mattiemus/IMC-Server/blob/master/IMC.hs (the exception gets thrown in sense)
  2. https://github.com/Mattiemus/IMC-Server/blob/master/IMC/Networking/Server.hs (Modifined within application handleClient, and cleanupClient)
  3. https://github.com/Mattiemus/IMC-Server/blob/master/IMC/Utilities/Concurrency.hs (functions which push and pop to a list stored in an MVar)

I am totally out of ideas, as I only use modifyMVar and withMVar (so surely it should never be totally left empty) - my only assumption is that maybe an Exception is being thrown while modifying the MVar, however I think this is highly unlikely.

Any help is appreciated, this problem has been bugging me for some time now.

3
I don't think most people will want to search through so much code to find this bug. Perhaps writing some tests will help you find it. - user2407038
I have modified the question to further pin down the problem to 4 main functions. What sort of tests would I need to write to isolate this problem, the problem seems to eventually occur regardless of weather i remove all modifiers over the either the server or events MVar, leading me to believe I am somehow using them incorrectly. There are multiple producers over the events MVar (one thread for each client) however only a single consumer - maybe this is the issue? - Mattiemus
In sense you are using an MVar very much like a channel. Why not use a Chan instead? - Chris Kuklewicz
modifyMVar and withMVar are safe. I do not see swapMVar used, which is unsafe, is it used elsewhere? - Chris Kuklewicz
@ChrisKuklewicz swapMVar was removed in favor of using modifyMVar_ due to this problem, however the core issue still persists. I dont use Chan because I dont want to block while the channel is empty, I want to use it as if there is simply an empty list of events. See popAllMVar in Concurrency.hs - Mattiemus

3 Answers

4
votes

Three days later and its solved: Was actually unrelated to either the networking or concurrency code, and infact caused by my incorrect re-implementation of Yampas dpSwitch in Netwire. Corrected code posted below for anyone wishing to implement this function:

dpSwitch :: (Monoid e, Applicative m, Monad m, T.Traversable col) => (forall wire. a -> col wire -> col (b, wire))
     -> col (Wire s e m b c)
     -> Wire s e m (a, col c) (Event d)
     -> (col (Wire s e m b c) -> d -> Wire s e m a (col c))
     -> Wire s e m a (col c)
dpSwitch route wireCol switchEvtGen continuation = WGen $ gen wireCol switchEvtGen
where
    gen wires switchEvtGenWire _ (Left x) = return (Left mempty, WGen $ gen wires switchEvtGenWire)
    gen wires switchEvtGenWire ws (Right x) = do            
        let routings = route x wires
        wireSteps <- T.sequenceA (fmap (\(wireInput, wire) -> stepWire wire ws (Right wireInput)) routings)
        let wireOutputs = T.sequenceA (fmap fst wireSteps)
            steppedWires = fmap snd wireSteps
        case wireOutputs of
            Left wireInhibitedOutput -> return (Left wireInhibitedOutput, WGen $ gen steppedWires switchEvtGenWire)
            Right wireResultOutput -> do
                (event, steppedSwitchEvtGenWire) <- stepWire switchEvtGenWire ws (Right (x, wireResultOutput))
                case event of
                    Left eventInhibited -> return (Left eventInhibited, WGen $ gen steppedWires steppedSwitchEvtGenWire)
                    Right NoEvent -> return (wireOutputs, WGen $ gen steppedWires steppedSwitchEvtGenWire)
                    Right (Event e) -> return (wireOutputs, continuation steppedWires e)
4
votes

For anyone who might stumble on this, some additional information thread blocked indefinitely in an MVar operation is not really that smart. It happens when every thread that contains a reference to the MVar is trying to read (or write) to that location, has died, or is waiting on another primitive that is blocked forever. eg thread 1 is trying to read MVar a and waiting on thread 2 which is either dead, also trying to read MVar a, or trying to read MVar b which can only be written to in thread 1.

The following code happily hangs forever:

do
  a <- newEmptyMVar
  forkIO (readMVar a >>= print)
  putMVar a $ last $ repeat 0
2
votes

I think I see the problem -- it is in Server.hs. You have operations that do network IO inside a withMVar call. Now imagine that IO blocks effectively forever. You neither get an exception that forces the var to be replaced, nor does the operation complete normally and replace the var, and hence you get stuck.

In general, you should not do any significant operations in a withMVar call, even though you can. And if you do any such operations, you need to be double-sure that you effectively guard them with timeouts, etc. such that you are sure they always complete one way or another.