Merge pull request #674 from phadej/delayed-cleanup-ioref-tests

Delayed cleanup ioref tests
This commit is contained in:
Oleg Grenrus 2017-01-18 10:36:12 +02:00 committed by GitHub
commit 6d0aa92517
4 changed files with 125 additions and 24 deletions

View file

@ -109,6 +109,7 @@ test-suite spec
Servant.ArbitraryMonadServerSpec Servant.ArbitraryMonadServerSpec
Servant.Server.ErrorSpec Servant.Server.ErrorSpec
Servant.Server.Internal.ContextSpec Servant.Server.Internal.ContextSpec
Servant.Server.Internal.RoutingApplicationSpec
Servant.Server.RouterSpec Servant.Server.RouterSpec
Servant.Server.StreamingSpec Servant.Server.StreamingSpec
Servant.Server.UsingContextSpec Servant.Server.UsingContextSpec

View file

@ -23,6 +23,7 @@ module Servant.Server.Internal
, module Servant.Server.Internal.ServantErr , module Servant.Server.Internal.ServantErr
) where ) where
import Control.Exception (finally)
import Control.Monad.Trans (liftIO) import Control.Monad.Trans (liftIO)
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Char8 as BC8
@ -400,11 +401,17 @@ instance HasServer Raw context where
type ServerT Raw m = Application type ServerT Raw m = Application
route Proxy _ rawApplication = RawRouter $ \ env request respond -> do route Proxy _ rawApplication = RawRouter $ \ env request respond -> do
r <- runDelayed rawApplication env request -- note: a Raw application doesn't register any cleanup
case r of -- but for the sake of consistency, we nonetheless run
Route app -> app request (respond . Route) -- the cleanup once its done
Fail a -> respond $ Fail a cleanupRef <- newCleanupRef
FailFatal e -> respond $ FailFatal e r <- runDelayed rawApplication env request cleanupRef
go r request respond `finally` runCleanup cleanupRef
where go r request respond = case r of
Route app -> app request (respond . Route)
Fail a -> respond $ Fail a
FailFatal e -> respond $ FailFatal e
-- | If you use 'ReqBody' in one of the endpoints for your API, -- | If you use 'ReqBody' in one of the endpoints for your API,
-- this automatically requires your server-side handler to be a function -- this automatically requires your server-side handler to be a function

View file

@ -6,10 +6,13 @@
{-# LANGUAGE KindSignatures #-} {-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
module Servant.Server.Internal.RoutingApplication where module Servant.Server.Internal.RoutingApplication where
import Control.Exception (finally)
import Control.Monad (ap, liftM) import Control.Monad (ap, liftM)
import Control.Monad.Trans (MonadIO(..)) import Control.Monad.Trans (MonadIO(..))
import Data.IORef (newIORef, readIORef, IORef, atomicModifyIORef)
import Network.Wai (Application, Request, import Network.Wai (Application, Request,
Response, ResponseReceived) Response, ResponseReceived)
import Prelude () import Prelude ()
@ -112,12 +115,33 @@ instance Functor (Delayed env) where
, .. , ..
} -- Note [Existential Record Update] } -- Note [Existential Record Update]
-- | A mutable cleanup action
newtype CleanupRef = CleanupRef (IORef (IO ()))
newCleanupRef :: IO CleanupRef
newCleanupRef = CleanupRef <$> newIORef (return ())
-- | Add a clean up action to a 'CleanupRef'
addCleanup' :: IO () -> CleanupRef -> IO ()
addCleanup' act (CleanupRef ref) = atomicModifyIORef ref (\act' -> (act' >> act, ()))
addCleanup :: IO () -> DelayedIO ()
addCleanup act = DelayedIO $ \_req cleanupRef ->
addCleanup' act cleanupRef >> return (Route ())
-- | Run all the clean up actions registered in
-- a 'CleanupRef'.
runCleanup :: CleanupRef -> IO ()
runCleanup (CleanupRef ref) = do
act <- readIORef ref
act
-- | Computations used in a 'Delayed' can depend on the -- | Computations used in a 'Delayed' can depend on the
-- incoming 'Request', may perform 'IO, and result in a -- incoming 'Request', may perform 'IO, and result in a
-- 'RouteResult, meaning they can either suceed, fail -- 'RouteResult, meaning they can either suceed, fail
-- (with the possibility to recover), or fail fatally. -- (with the possibility to recover), or fail fatally.
-- --
newtype DelayedIO a = DelayedIO { runDelayedIO :: Request -> IO (RouteResult a) } newtype DelayedIO a = DelayedIO { runDelayedIO :: Request -> CleanupRef -> IO (RouteResult a) }
instance Functor DelayedIO where instance Functor DelayedIO where
fmap = liftM fmap = liftM
@ -127,17 +151,17 @@ instance Applicative DelayedIO where
(<*>) = ap (<*>) = ap
instance Monad DelayedIO where instance Monad DelayedIO where
return x = DelayedIO (const $ return (Route x)) return x = DelayedIO (\_req _cleanup -> return (Route x))
DelayedIO m >>= f = DelayedIO m >>= f =
DelayedIO $ \ req -> do DelayedIO $ \ req cl -> do
r <- m req r <- m req cl
case r of case r of
Fail e -> return $ Fail e Fail e -> return $ Fail e
FailFatal e -> return $ FailFatal e FailFatal e -> return $ FailFatal e
Route a -> runDelayedIO (f a) req Route a -> runDelayedIO (f a) req cl
instance MonadIO DelayedIO where instance MonadIO DelayedIO where
liftIO m = DelayedIO (const $ Route <$> m) liftIO m = DelayedIO (\_req _cl -> Route <$> m)
-- | A 'Delayed' without any stored checks. -- | A 'Delayed' without any stored checks.
emptyDelayed :: RouteResult a -> Delayed env a emptyDelayed :: RouteResult a -> Delayed env a
@ -148,15 +172,15 @@ emptyDelayed result =
-- | Fail with the option to recover. -- | Fail with the option to recover.
delayedFail :: ServantErr -> DelayedIO a delayedFail :: ServantErr -> DelayedIO a
delayedFail err = DelayedIO (const $ return $ Fail err) delayedFail err = DelayedIO (\_req _cleanup -> return $ Fail err)
-- | Fail fatally, i.e., without any option to recover. -- | Fail fatally, i.e., without any option to recover.
delayedFailFatal :: ServantErr -> DelayedIO a delayedFailFatal :: ServantErr -> DelayedIO a
delayedFailFatal err = DelayedIO (const $ return $ FailFatal err) delayedFailFatal err = DelayedIO (\_req _cleanup -> return $ FailFatal err)
-- | Gain access to the incoming request. -- | Gain access to the incoming request.
withRequest :: (Request -> DelayedIO a) -> DelayedIO a withRequest :: (Request -> DelayedIO a) -> DelayedIO a
withRequest f = DelayedIO (\ req -> runDelayedIO (f req) req) withRequest f = DelayedIO (\ req cl -> runDelayedIO (f req) req cl)
-- | Add a capture to the end of the capture block. -- | Add a capture to the end of the capture block.
addCapture :: Delayed env (a -> b) addCapture :: Delayed env (a -> b)
@ -196,8 +220,8 @@ addBodyCheck :: Delayed env (a -> b)
-> Delayed env b -> Delayed env b
addBodyCheck Delayed{..} new = addBodyCheck Delayed{..} new =
Delayed Delayed
{ bodyD = (,) <$> bodyD <*> new { bodyD = (,) <$> bodyD <*> new
, serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req , serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
, .. , ..
} -- Note [Existential Record Update] } -- Note [Existential Record Update]
@ -240,13 +264,18 @@ passToServer Delayed{..} x =
runDelayed :: Delayed env a runDelayed :: Delayed env a
-> env -> env
-> Request -> Request
-> CleanupRef
-> IO (RouteResult a) -> IO (RouteResult a)
runDelayed Delayed{..} env = runDelayedIO $ do runDelayed Delayed{..} env req cleanupRef =
c <- capturesD env runDelayedIO
methodD (do c <- capturesD env
a <- authD methodD
b <- bodyD a <- authD
DelayedIO (\ req -> return $ serverD c a b req) b <- bodyD
DelayedIO $ \ r _cleanup -> return (serverD c a b r)
)
req
cleanupRef
-- | Runs a delayed server and the resulting action. -- | Runs a delayed server and the resulting action.
-- Takes a continuation that lets us send a response. -- Takes a continuation that lets us send a response.
@ -258,8 +287,11 @@ runAction :: Delayed env (Handler a)
-> (RouteResult Response -> IO r) -> (RouteResult Response -> IO r)
-> (a -> RouteResult Response) -> (a -> RouteResult Response)
-> IO r -> IO r
runAction action env req respond k = runAction action env req respond k = do
runDelayed action env req >>= go >>= respond cleanupRef <- newCleanupRef
(runDelayed action env req cleanupRef >>= go >>= respond)
`finally` runCleanup cleanupRef
where where
go (Fail e) = return $ Fail e go (Fail e) = return $ Fail e
go (FailFatal e) = return $ FailFatal e go (FailFatal e) = return $ FailFatal e

View file

@ -0,0 +1,61 @@
module Servant.Server.Internal.RoutingApplicationSpec (spec) where
import Prelude ()
import Prelude.Compat
import Control.Exception hiding (Handler)
import Control.Monad.IO.Class
import Data.Maybe (isJust)
import Data.IORef
import Servant.Server
import Servant.Server.Internal.RoutingApplication
import Test.Hspec
import System.IO.Unsafe (unsafePerformIO)
ok :: IO (RouteResult ())
ok = return (Route ())
-- Let's not write to the filesystem
delayedTestRef :: IORef (Maybe String)
delayedTestRef = unsafePerformIO $ newIORef Nothing
delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
delayed body srv = Delayed
{ capturesD = \() -> DelayedIO $ \_req _cl -> ok
, methodD = DelayedIO $ \_req_ _cl -> ok
, authD = DelayedIO $ \_req _cl -> ok
, bodyD = do
liftIO (writeIORef delayedTestRef (Just "hia") >> putStrLn "garbage created")
addCleanup (writeIORef delayedTestRef Nothing >> putStrLn "garbage collected")
body
, serverD = \() () _body _req -> srv
}
simpleRun :: Delayed () (Handler ())
-> IO ()
simpleRun d = fmap (either ignoreE id) . try $
runAction d () undefined (\_ -> return ()) (\_ -> FailFatal err500)
where ignoreE :: SomeException -> ()
ignoreE = const ()
spec :: Spec
spec = do
describe "Delayed" $ do
it "actually runs clean up actions" $ do
_ <- simpleRun $ delayed (return ()) (Route $ return ())
cleanUpDone <- isJust <$> readIORef delayedTestRef
cleanUpDone `shouldBe` False
it "even with exceptions in serverD" $ do
_ <- simpleRun $ delayed (return ()) (Route $ throw DivideByZero)
cleanUpDone <- isJust <$> readIORef delayedTestRef
cleanUpDone `shouldBe` False
it "even with routing failure in bodyD" $ do
_ <- simpleRun $ delayed (delayedFailFatal err500) (Route $ return ())
cleanUpDone <- isJust <$> readIORef delayedTestRef
cleanUpDone `shouldBe` False
it "even with exceptions in bodyD" $ do
_ <- simpleRun $ delayed (liftIO $ throwIO DivideByZero) (Route $ return ())
cleanUpDone <- isJust <$> readIORef delayedTestRef
cleanUpDone `shouldBe` False