grapesy-1.2.0: test-disconnect/Test/Disconnect/Util/Server.hs
module Test.Disconnect.Util.Server (
-- * Monitoring server handlers
HandlerResults(..)
, monitoredHandler
, getHandlerResults
) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception qualified as Exception
import Control.Monad
import GHC.Exception
import Network.GRPC.Common
import Network.GRPC.Server qualified as Server
{-------------------------------------------------------------------------------
Monitoring server handlers
-------------------------------------------------------------------------------}
data HandlerResults = HandlerResults{
-- | Number of normal terminations
handlerNormalTerminations :: TVar Int
-- | Number of terminations due to 'ClientDisconnected' exceptions
, handlerClientDisconnected :: TVar Int
-- | Terminations due to unexpected exceptions
, handlerUnexpectedExceptions :: TVar [SomeException]
}
-- | Construct 'TVar' that records the result of each handler invocation
monitoredHandler :: forall rpc.
( SupportsServerRpc rpc
, StaticMetadata (ResponseTrailingMetadata rpc)
, Default (ResponseInitialMetadata rpc)
)
=> (Server.Call rpc -> IO ())
-> IO (Server.SomeRpcHandler IO, HandlerResults)
monitoredHandler handler = do
handlerNormalTerminations <- newTVarIO 0
handlerClientDisconnected <- newTVarIO 0
handlerUnexpectedExceptions <- newTVarIO []
let handlerFailed :: SomeException -> IO ()
handlerFailed e = atomically $
case fromException e of
Just Server.ClientDisconnected{} ->
modifyTVar handlerClientDisconnected (+ 1)
_otherwise ->
modifyTVar handlerUnexpectedExceptions (e :)
handlerTerminated :: IO ()
handlerTerminated = atomically $
modifyTVar handlerNormalTerminations (+ 1)
return (
Server.someRpcHandler $ Server.mkRpcHandler @rpc $ \call ->
Exception.handle handlerFailed $ do
handler call
handlerTerminated
, HandlerResults{
handlerNormalTerminations
, handlerClientDisconnected
, handlerUnexpectedExceptions
}
)
-- | Get handler results
getHandlerResults ::
Int -- ^ Timeout
-> HandlerResults -- ^ Monitored handler
-> Int
-- ^ Number of expected results
--
-- Blocks when not all results are available yet (/client/ termination does
-- not guarantee that /server/ results are available, the handler might not
-- yet have got a chance to say that it is done).
-> IO (Int, Int, [SomeException])
getHandlerResults maxWait handlerResults expectedNumResults = do
timeoutVar <- newTVarIO False
void $ forkIO $ do
threadDelay maxWait
atomically $ writeTVar timeoutVar True
atomically $ do
normalTerminations <- readTVar handlerNormalTerminations
clientDisconnected <- readTVar handlerClientDisconnected
unexpectedExceptions <- readTVar handlerUnexpectedExceptions
timeout <- readTVar timeoutVar
let results = (
normalTerminations
, clientDisconnected
, unexpectedExceptions
)
numResults = sum [
normalTerminations
, clientDisconnected
, length unexpectedExceptions
]
if numResults == expectedNumResults then
return results
else if timeout then
throwSTM $ userError $ "getHandlerResults timeout: " ++ show results
else
retry
where
HandlerResults{
handlerNormalTerminations
, handlerClientDisconnected
, handlerUnexpectedExceptions
} = handlerResults