Skip to content

Commit 6d0aa92

Browse files
authored
Merge pull request #674 from phadej/delayed-cleanup-ioref-tests
Delayed cleanup ioref tests
2 parents cce0f59 + 60ee1ab commit 6d0aa92

File tree

4 files changed

+125
-24
lines changed

4 files changed

+125
-24
lines changed

servant-server/servant-server.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ test-suite spec
109109
Servant.ArbitraryMonadServerSpec
110110
Servant.Server.ErrorSpec
111111
Servant.Server.Internal.ContextSpec
112+
Servant.Server.Internal.RoutingApplicationSpec
112113
Servant.Server.RouterSpec
113114
Servant.Server.StreamingSpec
114115
Servant.Server.UsingContextSpec

servant-server/src/Servant/Server/Internal.hs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Servant.Server.Internal
2323
, module Servant.Server.Internal.ServantErr
2424
) where
2525

26+
import Control.Exception (finally)
2627
import Control.Monad.Trans (liftIO)
2728
import qualified Data.ByteString as B
2829
import qualified Data.ByteString.Char8 as BC8
@@ -400,11 +401,17 @@ instance HasServer Raw context where
400401
type ServerT Raw m = Application
401402

402403
route Proxy _ rawApplication = RawRouter $ \ env request respond -> do
403-
r <- runDelayed rawApplication env request
404-
case r of
405-
Route app -> app request (respond . Route)
406-
Fail a -> respond $ Fail a
407-
FailFatal e -> respond $ FailFatal e
404+
-- note: a Raw application doesn't register any cleanup
405+
-- but for the sake of consistency, we nonetheless run
406+
-- the cleanup once its done
407+
cleanupRef <- newCleanupRef
408+
r <- runDelayed rawApplication env request cleanupRef
409+
go r request respond `finally` runCleanup cleanupRef
410+
411+
where go r request respond = case r of
412+
Route app -> app request (respond . Route)
413+
Fail a -> respond $ Fail a
414+
FailFatal e -> respond $ FailFatal e
408415

409416
-- | If you use 'ReqBody' in one of the endpoints for your API,
410417
-- this automatically requires your server-side handler to be a function

servant-server/src/Servant/Server/Internal/RoutingApplication.hs

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
{-# LANGUAGE KindSignatures #-}
77
{-# LANGUAGE RecordWildCards #-}
88
{-# LANGUAGE StandaloneDeriving #-}
9+
{-# LANGUAGE TupleSections #-}
910
module Servant.Server.Internal.RoutingApplication where
1011

12+
import Control.Exception (finally)
1113
import Control.Monad (ap, liftM)
1214
import Control.Monad.Trans (MonadIO(..))
15+
import Data.IORef (newIORef, readIORef, IORef, atomicModifyIORef)
1316
import Network.Wai (Application, Request,
1417
Response, ResponseReceived)
1518
import Prelude ()
@@ -112,12 +115,33 @@ instance Functor (Delayed env) where
112115
, ..
113116
} -- Note [Existential Record Update]
114117

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+
115139
-- | Computations used in a 'Delayed' can depend on the
116140
-- incoming 'Request', may perform 'IO, and result in a
117141
-- 'RouteResult, meaning they can either suceed, fail
118142
-- (with the possibility to recover), or fail fatally.
119143
--
120-
newtype DelayedIO a = DelayedIO { runDelayedIO :: Request -> IO (RouteResult a) }
144+
newtype DelayedIO a = DelayedIO { runDelayedIO :: Request -> CleanupRef -> IO (RouteResult a) }
121145

122146
instance Functor DelayedIO where
123147
fmap = liftM
@@ -127,17 +151,17 @@ instance Applicative DelayedIO where
127151
(<*>) = ap
128152

129153
instance Monad DelayedIO where
130-
return x = DelayedIO (const $ return (Route x))
154+
return x = DelayedIO (\_req _cleanup -> return (Route x))
131155
DelayedIO m >>= f =
132-
DelayedIO $ \ req -> do
133-
r <- m req
156+
DelayedIO $ \ req cl -> do
157+
r <- m req cl
134158
case r of
135159
Fail e -> return $ Fail e
136160
FailFatal e -> return $ FailFatal e
137-
Route a -> runDelayedIO (f a) req
161+
Route a -> runDelayedIO (f a) req cl
138162

139163
instance MonadIO DelayedIO where
140-
liftIO m = DelayedIO (const $ Route <$> m)
164+
liftIO m = DelayedIO (\_req _cl -> Route <$> m)
141165

142166
-- | A 'Delayed' without any stored checks.
143167
emptyDelayed :: RouteResult a -> Delayed env a
@@ -148,15 +172,15 @@ emptyDelayed result =
148172

149173
-- | Fail with the option to recover.
150174
delayedFail :: ServantErr -> DelayedIO a
151-
delayedFail err = DelayedIO (const $ return $ Fail err)
175+
delayedFail err = DelayedIO (\_req _cleanup -> return $ Fail err)
152176

153177
-- | Fail fatally, i.e., without any option to recover.
154178
delayedFailFatal :: ServantErr -> DelayedIO a
155-
delayedFailFatal err = DelayedIO (const $ return $ FailFatal err)
179+
delayedFailFatal err = DelayedIO (\_req _cleanup -> return $ FailFatal err)
156180

157181
-- | Gain access to the incoming request.
158182
withRequest :: (Request -> DelayedIO a) -> DelayedIO a
159-
withRequest f = DelayedIO (\ req -> runDelayedIO (f req) req)
183+
withRequest f = DelayedIO (\ req cl -> runDelayedIO (f req) req cl)
160184

161185
-- | Add a capture to the end of the capture block.
162186
addCapture :: Delayed env (a -> b)
@@ -196,8 +220,8 @@ addBodyCheck :: Delayed env (a -> b)
196220
-> Delayed env b
197221
addBodyCheck Delayed{..} new =
198222
Delayed
199-
{ bodyD = (,) <$> bodyD <*> new
200-
, serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
223+
{ bodyD = (,) <$> bodyD <*> new
224+
, serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
201225
, ..
202226
} -- Note [Existential Record Update]
203227

@@ -240,13 +264,18 @@ passToServer Delayed{..} x =
240264
runDelayed :: Delayed env a
241265
-> env
242266
-> Request
267+
-> CleanupRef
243268
-> IO (RouteResult a)
244-
runDelayed Delayed{..} env = runDelayedIO $ do
245-
c <- capturesD env
246-
methodD
247-
a <- authD
248-
b <- bodyD
249-
DelayedIO (\ req -> return $ serverD c a b req)
269+
runDelayed Delayed{..} env req cleanupRef =
270+
runDelayedIO
271+
(do c <- capturesD env
272+
methodD
273+
a <- authD
274+
b <- bodyD
275+
DelayedIO $ \ r _cleanup -> return (serverD c a b r)
276+
)
277+
req
278+
cleanupRef
250279

251280
-- | Runs a delayed server and the resulting action.
252281
-- Takes a continuation that lets us send a response.
@@ -258,8 +287,11 @@ runAction :: Delayed env (Handler a)
258287
-> (RouteResult Response -> IO r)
259288
-> (a -> RouteResult Response)
260289
-> IO r
261-
runAction action env req respond k =
262-
runDelayed action env req >>= go >>= respond
290+
runAction action env req respond k = do
291+
cleanupRef <- newCleanupRef
292+
(runDelayed action env req cleanupRef >>= go >>= respond)
293+
`finally` runCleanup cleanupRef
294+
263295
where
264296
go (Fail e) = return $ Fail e
265297
go (FailFatal e) = return $ FailFatal e
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Servant.Server.Internal.RoutingApplicationSpec (spec) where
2+
3+
import Prelude ()
4+
import Prelude.Compat
5+
6+
import Control.Exception hiding (Handler)
7+
import Control.Monad.IO.Class
8+
import Data.Maybe (isJust)
9+
import Data.IORef
10+
import Servant.Server
11+
import Servant.Server.Internal.RoutingApplication
12+
import Test.Hspec
13+
14+
import System.IO.Unsafe (unsafePerformIO)
15+
16+
ok :: IO (RouteResult ())
17+
ok = return (Route ())
18+
19+
-- Let's not write to the filesystem
20+
delayedTestRef :: IORef (Maybe String)
21+
delayedTestRef = unsafePerformIO $ newIORef Nothing
22+
23+
delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
24+
delayed 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")
31+
body
32+
, serverD = \() () _body _req -> srv
33+
}
34+
35+
simpleRun :: Delayed () (Handler ())
36+
-> IO ()
37+
simpleRun d = fmap (either ignoreE id) . try $
38+
runAction d () undefined (\_ -> return ()) (\_ -> FailFatal err500)
39+
40+
where ignoreE :: SomeException -> ()
41+
ignoreE = const ()
42+
43+
spec :: Spec
44+
spec = do
45+
describe "Delayed" $ do
46+
it "actually runs clean up actions" $ do
47+
_ <- simpleRun $ delayed (return ()) (Route $ return ())
48+
cleanUpDone <- isJust <$> readIORef delayedTestRef
49+
cleanUpDone `shouldBe` False
50+
it "even with exceptions in serverD" $ do
51+
_ <- simpleRun $ delayed (return ()) (Route $ throw DivideByZero)
52+
cleanUpDone <- isJust <$> readIORef delayedTestRef
53+
cleanUpDone `shouldBe` False
54+
it "even with routing failure in bodyD" $ do
55+
_ <- simpleRun $ delayed (delayedFailFatal err500) (Route $ return ())
56+
cleanUpDone <- isJust <$> readIORef delayedTestRef
57+
cleanUpDone `shouldBe` False
58+
it "even with exceptions in bodyD" $ do
59+
_ <- simpleRun $ delayed (liftIO $ throwIO DivideByZero) (Route $ return ())
60+
cleanUpDone <- isJust <$> readIORef delayedTestRef
61+
cleanUpDone `shouldBe` False

0 commit comments

Comments
 (0)