on master help sign in
Merge pull request #675 from phadej/resourcet

Use resourcet for resource managment

commit8c3291355b0fc37bad89815e625099fb97e263d7 authorOleg Grenrus <oleg.grenrus@iki.fi> dateThu 19 Jan 2017, 10:27 (10 years ago) parent484bc9c6527937
4 files changed+199−90
servant-server/servant-server.cabal+5−3 servant-server/src/Servant/Server/Internal.hs+4−5 servant-server/src/Servant/Server/Internal/RoutingApplication.hs+93−61 servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+97−21
servant-server/servant-server.cabal+5−3
@@ -69,6 +69,7 @@ library
6969 , string-conversions >= 0.3 && < 0.5
7070 , system-filepath >= 0.4 && < 0.5
7171 , filepath >= 1 && < 1.5
72+ , resourcet >= 1.1.6 && <1.2
7273 , text >= 1.2 && < 1.3
7374 , transformers >= 0.3 && < 0.6
7475 , transformers-base >= 0.4.4 && < 0.5
@@ -127,19 +128,20 @@ test-suite spec
127128 , hspec == 2.*
128129 , hspec-wai >= 0.8 && <0.9
129130 , http-types
131+ , mtl
130132 , network >= 2.6
131− , QuickCheck
132133 , parsec
134+ , QuickCheck
135+ , resourcet
133136 , safe
134137 , servant
135138 , servant-server
136− , string-conversions
137139 , should-not-typecheck == 2.1.*
140+ , string-conversions
138141 , temporary
139142 , text
140143 , transformers
141144 , transformers-compat
142− , mtl
143145 , wai
144146 , wai-extra
145147 , warp
servant-server/src/Servant/Server/Internal.hs+4−5
@@ -23,8 +23,8 @@ module Servant.Server.Internal
2323 , module Servant.Server.Internal.ServantErr
2424 ) where
2525
26−import Control.Exception (finally)
2726import Control.Monad.Trans (liftIO)
27+import Control.Monad.Trans.Resource (runResourceT)
2828import qualified Data.ByteString as B
2929import qualified Data.ByteString.Char8 as BC8
3030import qualified Data.ByteString.Lazy as BL
@@ -400,13 +400,12 @@ instance HasServer Raw context where
400400
401401 type ServerT Raw m = Application
402402
403− route Proxy _ rawApplication = RawRouter $ \ env request respond -> do
403+ route Proxy _ rawApplication = RawRouter $ \ env request respond -> runResourceT $ do
404404 -- note: a Raw application doesn't register any cleanup
405405 -- but for the sake of consistency, we nonetheless run
406406 -- the cleanup once its done
407− cleanupRef <- newCleanupRef
408− r <- runDelayed rawApplication env request cleanupRef
409− go r request respond `finally` runCleanup cleanupRef
407+ r <- runDelayed rawApplication env request
408+ liftIO $ go r request respond
410409
411410 where go r request respond = case r of
412411 Route app -> app request (respond . Route)
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+93−61
@@ -1,24 +1,32 @@
11{-# LANGUAGE CPP #-}
22{-# LANGUAGE DeriveFunctor #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE FlexibleInstances #-}
54{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
66{-# LANGUAGE KindSignatures #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
8+{-# LANGUAGE OverloadedStrings #-}
79{-# LANGUAGE RecordWildCards #-}
810{-# LANGUAGE StandaloneDeriving #-}
911{-# LANGUAGE TupleSections #-}
12+{-# LANGUAGE TypeFamilies #-}
13+{-# LANGUAGE TypeOperators #-}
14+{-# LANGUAGE UndecidableInstances #-}
1015module Servant.Server.Internal.RoutingApplication where
1116
12−import Control.Exception (finally)
1317import Control.Monad (ap, liftM)
14−import Control.Monad.Trans (MonadIO(..))
15−import Data.IORef (newIORef, readIORef, IORef, atomicModifyIORef)
16−import Network.Wai (Application, Request,
17− Response, ResponseReceived)
18+import Control.Monad.Base (MonadBase (..))
19+import Control.Monad.Catch (MonadThrow (..))
20+import Control.Monad.Reader (MonadReader (..), ReaderT, runReaderT)
21+import Control.Monad.Trans (MonadIO (..), MonadTrans (..))
22+import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl (..), MonadTransControl (..),
23+ defaultLiftBaseWith, defaultRestoreM)
24+import Control.Monad.Trans.Resource (MonadResource (..), ResourceT, runResourceT, transResourceT)
25+import Network.Wai (Application, Request, Response, ResponseReceived)
1826import Prelude ()
1927import Prelude.Compat
20−import Servant.Server.Internal.ServantErr
2128import Servant.Server.Internal.Handler
29+import Servant.Server.Internal.ServantErr
2230
2331type RoutingApplication =
2432 Request -- ^ the request, the field 'pathInfo' may be modified by url routing
@@ -32,6 +40,55 @@ data RouteResult a =
3240 | Route !a
3341 deriving (Eq, Show, Read, Functor)
3442
43+instance Applicative RouteResult where
44+ pure = return
45+ (<*>) = ap
46+
47+instance Monad RouteResult where
48+ return = Route
49+ Route a >>= f = f a
50+ Fail e >>= _ = Fail e
51+ FailFatal e >>= _ = FailFatal e
52+
53+newtype RouteResultT m a = RouteResultT { runRouteResultT :: m (RouteResult a) }
54+ deriving (Functor)
55+
56+instance MonadTrans RouteResultT where
57+ lift = RouteResultT . liftM Route
58+
59+instance (Functor m, Monad m) => Applicative (RouteResultT m) where
60+ pure = return
61+ (<*>) = ap
62+
63+instance Monad m => Monad (RouteResultT m) where
64+ return = RouteResultT . return . Route
65+ m >>= k = RouteResultT $ do
66+ a <- runRouteResultT m
67+ case a of
68+ Fail e -> return $ Fail e
69+ FailFatal e -> return $ FailFatal e
70+ Route b -> runRouteResultT (k b)
71+
72+instance MonadIO m => MonadIO (RouteResultT m) where
73+ liftIO = lift . liftIO
74+
75+instance MonadBase b m => MonadBase b (RouteResultT m) where
76+ liftBase = lift . liftBase
77+
78+instance MonadBaseControl b m => MonadBaseControl b (RouteResultT m) where
79+ type StM (RouteResultT m) a = ComposeSt RouteResultT m a
80+ liftBaseWith = defaultLiftBaseWith
81+ restoreM = defaultRestoreM
82+
83+instance MonadTransControl RouteResultT where
84+ type StT RouteResultT a = RouteResult a
85+ liftWith f = RouteResultT $ liftM return $ f $ runRouteResultT
86+ restoreT = RouteResultT
87+
88+instance MonadThrow m => MonadThrow (RouteResultT m) where
89+ throwM = lift . throwM
90+
91+
3592toApplication :: RoutingApplication -> Application
3693toApplication ra request respond = ra request routingRespond
3794 where
@@ -115,53 +172,30 @@ instance Functor (Delayed env) where
115172 , ..
116173 } -- Note [Existential Record Update]
117174
118−-- | A mutable cleanup action
119−newtype CleanupRef = CleanupRef (IORef (IO ()))
120−
121−newCleanupRef :: IO CleanupRef
122−newCleanupRef = CleanupRef <$> newIORef (return ())
123−
124−-- | Add a clean up action to a 'CleanupRef'
125−addCleanup' :: IO () -> CleanupRef -> IO ()
126−addCleanup' act (CleanupRef ref) = atomicModifyIORef ref (\act' -> (act' >> act, ()))
127−
128−addCleanup :: IO () -> DelayedIO ()
129−addCleanup act = DelayedIO $ \_req cleanupRef ->
130− addCleanup' act cleanupRef >> return (Route ())
131−
132−-- | Run all the clean up actions registered in
133−-- a 'CleanupRef'.
134−runCleanup :: CleanupRef -> IO ()
135−runCleanup (CleanupRef ref) = do
136− act <- readIORef ref
137− act
138−
139175-- | Computations used in a 'Delayed' can depend on the
140176-- incoming 'Request', may perform 'IO, and result in a
141177-- 'RouteResult, meaning they can either suceed, fail
142178-- (with the possibility to recover), or fail fatally.
143179--
144−newtype DelayedIO a = DelayedIO { runDelayedIO :: Request -> CleanupRef -> IO (RouteResult a) }
145−
146−instance Functor DelayedIO where
147− fmap = liftM
180+newtype DelayedIO a = DelayedIO { runDelayedIO' :: ReaderT Request (ResourceT (RouteResultT IO)) a }
181+ deriving
182+ ( Functor, Applicative, Monad
183+ , MonadIO, MonadReader Request
184+ , MonadBase IO
185+ , MonadThrow
186+ , MonadResource
187+ )
148188
149−instance Applicative DelayedIO where
150− pure = return
151− (<*>) = ap
189+liftRouteResult :: RouteResult a -> DelayedIO a
190+liftRouteResult x = DelayedIO $ lift . lift $ RouteResultT . return $ x
152191
153−instance Monad DelayedIO where
154− return x = DelayedIO (\_req _cleanup -> return (Route x))
155− DelayedIO m >>= f =
156− DelayedIO $ \ req cl -> do
157− r <- m req cl
158− case r of
159− Fail e -> return $ Fail e
160− FailFatal e -> return $ FailFatal e
161− Route a -> runDelayedIO (f a) req cl
192+instance MonadBaseControl IO DelayedIO where
193+ type StM DelayedIO a = StM (ReaderT Request (ResourceT (RouteResultT IO))) a
194+ liftBaseWith f = DelayedIO $ liftBaseWith $ \g -> f (g . runDelayedIO')
195+ restoreM = DelayedIO . restoreM
162196
163−instance MonadIO DelayedIO where
164− liftIO m = DelayedIO (\_req _cl -> Route <$> m)
197+runDelayedIO :: DelayedIO a -> Request -> ResourceT IO (RouteResult a)
198+runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO' m) req
165199
166200-- | A 'Delayed' without any stored checks.
167201emptyDelayed :: RouteResult a -> Delayed env a
@@ -172,15 +206,17 @@ emptyDelayed result =
172206
173207-- | Fail with the option to recover.
174208delayedFail :: ServantErr -> DelayedIO a
175−delayedFail err = DelayedIO (\_req _cleanup -> return $ Fail err)
209+delayedFail err = liftRouteResult $ Fail err
176210
177211-- | Fail fatally, i.e., without any option to recover.
178212delayedFailFatal :: ServantErr -> DelayedIO a
179−delayedFailFatal err = DelayedIO (\_req _cleanup -> return $ FailFatal err)
213+delayedFailFatal err = liftRouteResult $ FailFatal err
180214
181215-- | Gain access to the incoming request.
182216withRequest :: (Request -> DelayedIO a) -> DelayedIO a
183−withRequest f = DelayedIO (\ req cl -> runDelayedIO (f req) req cl)
217+withRequest f = do
218+ req <- ask
219+ f req
184220
185221-- | Add a capture to the end of the capture block.
186222addCapture :: Delayed env (a -> b)
@@ -264,18 +300,17 @@ passToServer Delayed{..} x =
264300runDelayed :: Delayed env a
265301 -> env
266302 -> Request
267− -> CleanupRef
268− -> IO (RouteResult a)
269−runDelayed Delayed{..} env req cleanupRef =
303+ -> ResourceT IO (RouteResult a)
304+runDelayed Delayed{..} env req =
270305 runDelayedIO
271306 (do c <- capturesD env
272307 methodD
273308 a <- authD
274309 b <- bodyD
275− DelayedIO $ \ r _cleanup -> return (serverD c a b r)
310+ r <- ask
311+ liftRouteResult (serverD c a b r)
276312 )
277313 req
278− cleanupRef
279314
280315-- | Runs a delayed server and the resulting action.
281316-- Takes a continuation that lets us send a response.
@@ -287,15 +322,12 @@ runAction :: Delayed env (Handler a)
287322 -> (RouteResult Response -> IO r)
288323 -> (a -> RouteResult Response)
289324 -> IO r
290−runAction action env req respond k = do
291− cleanupRef <- newCleanupRef
292− (runDelayed action env req cleanupRef >>= go >>= respond)
293− `finally` runCleanup cleanupRef
294−
325+runAction action env req respond k = runResourceT $ do
326+ runDelayed action env req >>= go >>= liftIO . respond
295327 where
296328 go (Fail e) = return $ Fail e
297329 go (FailFatal e) = return $ FailFatal e
298− go (Route a) = do
330+ go (Route a) = liftIO $ do
299331 e <- runHandler a
300332 case e of
301333 Left err -> return . Route $ responseServantErr err
servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+97−21
@@ -1,35 +1,69 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE KindSignatures #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE TypeOperators #-}
19module Servant.Server.Internal.RoutingApplicationSpec (spec) where
210
311import Prelude ()
412import Prelude.Compat
513
614import Control.Exception hiding (Handler)
15+import Control.Monad.Trans.Resource (register)
716import Control.Monad.IO.Class
8−import Data.Maybe (isJust)
917import Data.IORef
10−import Servant.Server
18+import Data.Proxy
19+import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
20+import Servant
1121import Servant.Server.Internal.RoutingApplication
1222import Test.Hspec
23+import Test.Hspec.Wai (request, shouldRespondWith, with)
24+
25+import qualified Data.Text as T
1326
1427import System.IO.Unsafe (unsafePerformIO)
1528
16−ok :: IO (RouteResult ())
17−ok = return (Route ())
29+data TestResource x
30+ = TestResourceNone
31+ | TestResource x
32+ | TestResourceFreed
33+ | TestResourceError
34+ deriving (Eq, Show)
1835
1936-- Let's not write to the filesystem
20−delayedTestRef :: IORef (Maybe String)
21−delayedTestRef = unsafePerformIO $ newIORef Nothing
37+delayedTestRef :: IORef (TestResource String)
38+delayedTestRef = unsafePerformIO $ newIORef TestResourceNone
39+
40+fromTestResource :: a -> (b -> a) -> TestResource b -> a
41+fromTestResource _ f (TestResource x) = f x
42+fromTestResource x _ _ = x
43+
44+initTestResource :: IO ()
45+initTestResource = writeIORef delayedTestRef TestResourceNone
46+
47+writeTestResource :: String -> IO ()
48+writeTestResource x = modifyIORef delayedTestRef $ \r -> case r of
49+ TestResourceNone -> TestResource x
50+ _ -> TestResourceError
51+
52+freeTestResource :: IO ()
53+freeTestResource = modifyIORef delayedTestRef $ \r -> case r of
54+ TestResource _ -> TestResourceFreed
55+ _ -> TestResourceError
2256
2357delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
2458delayed body srv = Delayed
25− { capturesD = \() -> DelayedIO $ \_req _cl -> ok
26− , methodD = DelayedIO $ \_req_ _cl -> ok
27− , authD = DelayedIO $ \_req _cl -> ok
28− , bodyD = do
29− liftIO (writeIORef delayedTestRef (Just "hia") >> putStrLn "garbage created")
30− addCleanup (writeIORef delayedTestRef Nothing >> putStrLn "garbage collected")
59+ { capturesD = \_ -> return ()
60+ , methodD = return ()
61+ , authD = return ()
62+ , bodyD = do
63+ liftIO (writeTestResource"hia" >> putStrLn "garbage created")
64+ _ <- register (freeTestResource >> putStrLn "garbage collected")
3165 body
32− , serverD = \() () _body _req -> srv
66+ , serverD = \() () _body _req -> srv
3367 }
3468
3569simpleRun :: Delayed () (Handler ())
@@ -40,22 +74,64 @@ simpleRun d = fmap (either ignoreE id) . try $
4074 where ignoreE :: SomeException -> ()
4175 ignoreE = const ()
4276
77+-------------------------------------------------------------------------------
78+-- Combinator example
79+-------------------------------------------------------------------------------
80+
81+-- | This data types writes 'sym' to 'delayedTestRef'.
82+data Res (sym :: Symbol)
83+
84+instance (KnownSymbol sym, HasServer api ctx) => HasServer (Res sym :> api) ctx where
85+ type ServerT (Res sym :> api) m = IORef (TestResource String) -> ServerT api m
86+ route Proxy ctx server = route (Proxy :: Proxy api) ctx $
87+ server `addBodyCheck` check
88+ where
89+ sym = symbolVal (Proxy :: Proxy sym)
90+ check = do
91+ liftIO $ writeTestResource sym
92+ _ <- register freeTestResource
93+ return delayedTestRef
94+
95+type ResApi = "foobar" :> Res "foobar" :> Get '[PlainText] T.Text
96+
97+resApi :: Proxy ResApi
98+resApi = Proxy
99+
100+resServer :: Server ResApi
101+resServer ref = liftIO $ fmap (fromTestResource "<wrong>" T.pack) $ readIORef ref
102+
103+-------------------------------------------------------------------------------
104+-- Spec
105+-------------------------------------------------------------------------------
106+
43107spec :: Spec
44108spec = do
45109 describe "Delayed" $ do
46110 it "actually runs clean up actions" $ do
111+ liftIO initTestResource
47112 _ <- simpleRun $ delayed (return ()) (Route $ return ())
48− cleanUpDone <- isJust <$> readIORef delayedTestRef
49− cleanUpDone `shouldBe` False
113+ res <- readIORef delayedTestRef
114+ res `shouldBe` TestResourceFreed
50115 it "even with exceptions in serverD" $ do
116+ liftIO initTestResource
51117 _ <- simpleRun $ delayed (return ()) (Route $ throw DivideByZero)
52− cleanUpDone <- isJust <$> readIORef delayedTestRef
53− cleanUpDone `shouldBe` False
118+ res <- readIORef delayedTestRef
119+ res `shouldBe` TestResourceFreed
54120 it "even with routing failure in bodyD" $ do
121+ liftIO initTestResource
55122 _ <- simpleRun $ delayed (delayedFailFatal err500) (Route $ return ())
56− cleanUpDone <- isJust <$> readIORef delayedTestRef
57− cleanUpDone `shouldBe` False
123+ res <- readIORef delayedTestRef
124+ res `shouldBe` TestResourceFreed
58125 it "even with exceptions in bodyD" $ do
126+ liftIO initTestResource
59127 _ <- simpleRun $ delayed (liftIO $ throwIO DivideByZero) (Route $ return ())
60− cleanUpDone <- isJust <$> readIORef delayedTestRef
61− cleanUpDone `shouldBe` False
128+ res <- readIORef delayedTestRef
129+ res `shouldBe` TestResourceFreed
130+ describe "ResApi" $
131+ with (return $ serve resApi resServer) $ do
132+ it "writes and cleanups resources" $ do
133+ liftIO initTestResource
134+ request "GET" "foobar" [] "" `shouldRespondWith` "foobar"
135+ liftIO $ do
136+ res <- readIORef delayedTestRef
137+ res `shouldBe` TestResourceFreed