Fix race condition while tracking outstanding handler threads (#66)

* Demonstrate shutdown concurrency bug via unregistered server streaming test

* Comments, cleanup

* Remove extraneous liftIOs from low level tests
This commit is contained in:
Joel Stanley 2016-08-10 16:14:41 -05:00 committed by GitHub Enterprise
parent d618b90481
commit d7b00ac054
3 changed files with 32 additions and 27 deletions

View file

@ -8,6 +8,7 @@ GRPC
, GRPCIOError(..) , GRPCIOError(..)
, throwIfCallError , throwIfCallError
, grpcDebug , grpcDebug
, grpcDebug'
, threadDelaySecs , threadDelaySecs
, C.MetadataMap(..) , C.MetadataMap(..)
, StatusDetails(..) , StatusDetails(..)

View file

@ -82,12 +82,22 @@ forkServer Server{..} f = do
case shutdown of case shutdown of
True -> return False True -> return False
False -> do False -> do
tid <- forkFinally f cleanup -- NB: The spawned thread waits on 'ready' before running 'f' to ensure
atomically $ modifyTVar' outstandingForks (S.insert tid) -- that its ThreadId is inserted into outstandingForks before the cleanup
-- function deletes it from there. Not doing this can lead to stale thread
-- ids in the set when handlers cleanup ahead of the insertion, and a
-- subsequent deadlock in stopServer. We can use a dead-list instead if we
-- need something more performant.
ready <- newTVarIO False
tid <- let act = do atomically (check =<< readTVar ready)
f
in forkFinally act cleanup
atomically $ do
modifyTVar' outstandingForks (S.insert tid)
modifyTVar' ready (const True)
#ifdef DEBUG #ifdef DEBUG
currSet <- readTVarIO outstandingForks tids <- readTVarIO outstandingForks
grpcDebug $ "forkServer: number of outstandingForks is " grpcDebug $ "after fork and bookkeeping: outstandingForks=" ++ show tids
++ show (S.size currSet)
#endif #endif
return True return True
where cleanup _ = do where cleanup _ = do
@ -176,16 +186,17 @@ stopServer Server{ unsafeServer = s, .. } = do
case shutdownEvent of case shutdownEvent of
-- This case occurs when we pluck but the queue is already in the -- This case occurs when we pluck but the queue is already in the
-- 'shuttingDown' state, implying we already tried to shut down. -- 'shuttingDown' state, implying we already tried to shut down.
(Left GRPCIOShutdown) -> error "Called stopServer twice!" Left GRPCIOShutdown -> error "Called stopServer twice!"
(Left _) -> error "Failed to stop server." Left _ -> error "Failed to stop server."
(Right _) -> return () Right _ -> return ()
cleanupForks = do cleanupForks = do
atomically $ writeTVar serverShuttingDown True atomically $ writeTVar serverShuttingDown True
liveForks <- readTVarIO outstandingForks liveForks <- readTVarIO outstandingForks
grpcDebug $ "Server shutdown: killing threads: " ++ show liveForks grpcDebug $ "Server shutdown: killing threads: " ++ show liveForks
mapM_ killThread liveForks mapM_ killThread liveForks
--wait for threads to shut down. -- wait for threads to shut down
atomically $ readTVar outstandingForks >>= (check . (==0) . S.size) grpcDebug "Server shutdown: waiting until all threads are dead."
atomically $ check . (==0) . S.size =<< readTVar outstandingForks
grpcDebug "Server shutdown: All forks cleaned up." grpcDebug "Server shutdown: All forks cleaned up."
-- Uses 'bracket' to safely start and stop a server, even if exceptions occur. -- Uses 'bracket' to safely start and stop a server, even if exceptions occur.

View file

@ -179,7 +179,7 @@ testServerStreaming =
client c = do client c = do
rm <- clientRegisterMethodServerStreaming c "/feed" rm <- clientRegisterMethodServerStreaming c "/feed"
eea <- clientReader c rm 10 clientPay clientInitMD $ \initMD recv -> do eea <- clientReader c rm 10 clientPay clientInitMD $ \initMD recv -> do
liftIO $ checkMD "Server initial metadata mismatch" serverInitMD initMD checkMD "Server initial metadata mismatch" serverInitMD initMD
forM_ pays $ \p -> recv `is` Right (Just p) forM_ pays $ \p -> recv `is` Right (Just p)
recv `is` Right Nothing recv `is` Right Nothing
eea @?= Right (dummyMeta, StatusOk, "dtls") eea @?= Right (dummyMeta, StatusOk, "dtls")
@ -187,9 +187,7 @@ testServerStreaming =
server s = do server s = do
let rm = head (sstreamingMethods s) let rm = head (sstreamingMethods s)
r <- serverWriter s rm serverInitMD $ \sc send -> do r <- serverWriter s rm serverInitMD $ \sc send -> do
liftIO $ do checkMD "Server request metadata mismatch" clientInitMD (metadata sc)
checkMD "Server request metadata mismatch"
clientInitMD (metadata sc)
payload sc @?= clientPay payload sc @?= clientPay
forM_ pays $ \p -> send p `is` Right () forM_ pays $ \p -> send p `is` Right ()
return (dummyMeta, StatusOk, "dtls") return (dummyMeta, StatusOk, "dtls")
@ -211,16 +209,14 @@ testServerStreamingUnregistered =
client c = do client c = do
rm <- clientRegisterMethodServerStreaming c "/feed" rm <- clientRegisterMethodServerStreaming c "/feed"
eea <- clientReader c rm 10 clientPay clientInitMD $ \initMD recv -> do eea <- clientReader c rm 10 clientPay clientInitMD $ \initMD recv -> do
liftIO $ checkMD "Server initial metadata mismatch" serverInitMD initMD checkMD "Server initial metadata mismatch" serverInitMD initMD
forM_ pays $ \p -> recv `is` Right (Just p) forM_ pays $ \p -> recv `is` Right (Just p)
recv `is` Right Nothing recv `is` Right Nothing
eea @?= Right (dummyMeta, StatusOk, "dtls") eea @?= Right (dummyMeta, StatusOk, "dtls")
server s = U.withServerCallAsync s $ \call -> do server s = U.withServerCallAsync s $ \call -> do
r <- U.serverWriter s call serverInitMD $ \sc send -> do r <- U.serverWriter s call serverInitMD $ \sc send -> do
liftIO $ do checkMD "Server request metadata mismatch" clientInitMD (metadata sc)
checkMD "Server request metadata mismatch"
clientInitMD (metadata sc)
payload sc @?= clientPay payload sc @?= clientPay
forM_ pays $ \p -> send p `is` Right () forM_ pays $ \p -> send p `is` Right ()
return (dummyMeta, StatusOk, "dtls") return (dummyMeta, StatusOk, "dtls")
@ -248,8 +244,7 @@ testClientStreaming =
server s = do server s = do
let rm = head (cstreamingMethods s) let rm = head (cstreamingMethods s)
eea <- serverReader s rm serverInitMD $ \sc recv -> do eea <- serverReader s rm serverInitMD $ \sc recv -> do
liftIO $ checkMD "Client request metadata mismatch" checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
clientInitMD (metadata sc)
forM_ pays $ \p -> recv `is` Right (Just p) forM_ pays $ \p -> recv `is` Right (Just p)
recv `is` Right Nothing recv `is` Right Nothing
return (Just serverRsp, trailMD, serverStatus, serverDtls) return (Just serverRsp, trailMD, serverStatus, serverDtls)
@ -276,8 +271,7 @@ testClientStreamingUnregistered =
server s = U.withServerCallAsync s $ \call -> do server s = U.withServerCallAsync s $ \call -> do
eea <- U.serverReader s call serverInitMD $ \sc recv -> do eea <- U.serverReader s call serverInitMD $ \sc recv -> do
liftIO $ checkMD "Client request metadata mismatch" checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
clientInitMD (metadata sc)
forM_ pays $ \p -> recv `is` Right (Just p) forM_ pays $ \p -> recv `is` Right (Just p)
recv `is` Right Nothing recv `is` Right Nothing
return (Just serverRsp, trailMD, serverStatus, serverDtls) return (Just serverRsp, trailMD, serverStatus, serverDtls)
@ -308,8 +302,7 @@ testBiDiStreaming =
server s = do server s = do
let rm = head (bidiStreamingMethods s) let rm = head (bidiStreamingMethods s)
eea <- serverRW s rm serverInitMD $ \sc recv send -> do eea <- serverRW s rm serverInitMD $ \sc recv send -> do
liftIO $ checkMD "Client request metadata mismatch" checkMD "Client request metadata mismatch" clientInitMD (metadata sc)
clientInitMD (metadata sc)
recv `is` Right (Just "cw0") recv `is` Right (Just "cw0")
send "sw0" `is` Right () send "sw0" `is` Right ()
recv `is` Right (Just "cw1") recv `is` Right (Just "cw1")