Skip to content

Commit 37482d6

Browse files
committed
Test that Stream combinator doesn't blow up memory.
1 parent 624a42e commit 37482d6

File tree

4 files changed

+70
-36
lines changed

4 files changed

+70
-36
lines changed

servant-client/servant-client.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ library
8282

8383
test-suite spec
8484
type: exitcode-stdio-1.0
85-
ghc-options: -Wall
85+
ghc-options: -Wall -rtsopts -with-rtsopts=-T
8686
default-language: Haskell2010
8787
hs-source-dirs: test
8888
main-is: Spec.hs

servant-client/src/Servant/Client/Internal/HttpClient.hs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,18 @@ requestToClientRequest burl r = Client.defaultRequest
183183
where
184184
hs = toList $ requestAccept r
185185

186+
convertBody bd = case bd of
187+
RequestBodyLBS body' -> Client.RequestBodyLBS body'
188+
RequestBodyBS body' -> Client.RequestBodyBS body'
189+
RequestBodyBuilder size body' -> Client.RequestBodyBuilder size body'
190+
RequestBodyStream size body' -> Client.RequestBodyStream size body'
191+
RequestBodyStreamChunked body' -> Client.RequestBodyStreamChunked body'
192+
RequestBodyIO body' -> Client.RequestBodyIO (convertBody <$> body')
193+
186194
(body, contentTypeHdr) = case requestBody r of
187195
Nothing -> (Client.RequestBodyLBS "", Nothing)
188-
Just (RequestBodyLBS body', typ)
189-
-> (Client.RequestBodyLBS body', Just (hContentType, renderHeader typ))
196+
Just (body', typ)
197+
-> (convertBody body', Just (hContentType, renderHeader typ))
190198

191199
isSecure = case baseUrlScheme burl of
192200
Http -> False

servant-client/test/Servant/ClientSpec.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ instance FromJSON Person
103103
instance ToForm Person
104104
instance FromForm Person
105105

106+
instance Arbitrary Person where
107+
arbitrary = Person <$> arbitrary <*> arbitrary
108+
106109
alice :: Person
107110
alice = Person "Alice" 42
108111

servant-client/test/Servant/StreamSpec.hs

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,26 @@
2626
#include "overlapping-compat.h"
2727
module Servant.StreamSpec (spec) where
2828

29-
import Prelude ()
30-
import Prelude.Compat
29+
import Control.Monad (replicateM_, void)
30+
import qualified Data.ByteString as BS
3131
import Data.Proxy
32-
import qualified Network.HTTP.Client as C
33-
import System.IO.Unsafe (unsafePerformIO)
32+
import GHC.Stats (currentBytesUsed, getGCStats)
33+
import qualified Network.HTTP.Client as C
34+
import Prelude ()
35+
import Prelude.Compat
36+
import System.IO (IOMode (ReadMode), withFile)
37+
import System.IO.Unsafe (unsafePerformIO)
3438
import Test.Hspec
39+
import Test.QuickCheck
3540

36-
import Servant.API ((:<|>) ((:<|>)),
37-
(:>),
38-
EmptyAPI, JSON,
39-
StreamGet,
40-
NewlineFraming,
41-
NetstringFraming,
42-
ResultStream(..),
43-
StreamGenerator(..))
41+
import Servant.API ((:<|>) ((:<|>)), (:>), JSON,
42+
NetstringFraming, NewlineFraming,
43+
OctetStream, ResultStream (..),
44+
StreamGenerator (..), StreamGet)
4445
import Servant.Client
46+
import Servant.ClientSpec (Person (..))
47+
import qualified Servant.ClientSpec as CS
4548
import Servant.Server
46-
import qualified Servant.ClientSpec as CS
47-
import Servant.ClientSpec (Person(..))
4849

4950

5051
spec :: Spec
@@ -54,7 +55,7 @@ spec = describe "Servant.Stream" $ do
5455
type StreamApi f =
5556
"streamGetNewline" :> StreamGet NewlineFraming JSON (f Person)
5657
:<|> "streamGetNetstring" :> StreamGet NetstringFraming JSON (f Person)
57-
:<|> EmptyAPI
58+
:<|> "streamALot" :> StreamGet NewlineFraming OctetStream (f BS.ByteString)
5859

5960

6061
capi :: Proxy (StreamApi ResultStream)
@@ -63,12 +64,9 @@ capi = Proxy
6364
sapi :: Proxy (StreamApi StreamGenerator)
6465
sapi = Proxy
6566

66-
67-
getGetNL :<|> getGetNS :<|> EmptyClient = client capi
68-
69-
70-
getGetNL :: ClientM (ResultStream Person)
71-
getGetNS :: ClientM (ResultStream Person)
67+
getGetNL, getGetNS :: ClientM (ResultStream Person)
68+
getGetALot :: ClientM (ResultStream BS.ByteString)
69+
getGetNL :<|> getGetNS :<|> getGetALot = client capi
7270

7371
alice :: Person
7472
alice = Person "Alice" 42
@@ -77,14 +75,24 @@ bob :: Person
7775
bob = Person "Bob" 25
7876

7977
server :: Application
80-
server = serve sapi (
81-
(return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
82-
:: Handler (StreamGenerator Person))
83-
:<|>
84-
(return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
85-
:: Handler (StreamGenerator Person))
86-
:<|>
87-
emptyServer)
78+
server = serve sapi
79+
$ return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
80+
:<|> return (StreamGenerator (\f r -> f alice >> r bob >> r alice))
81+
:<|> return (StreamGenerator lotsGenerator)
82+
where
83+
lotsGenerator f r = do
84+
f ""
85+
withFile "/dev/urandom" ReadMode $
86+
\handle -> streamFiveMBNTimes handle 1000 r
87+
return ()
88+
89+
streamFiveMBNTimes handle left sink
90+
| left <= 0 = return ""
91+
| otherwise = do
92+
msg <- BS.hGet handle (megabytes 5)
93+
sink msg
94+
streamFiveMBNTimes handle (left - 1) sink
95+
8896

8997

9098
{-# NOINLINE manager' #-}
@@ -94,20 +102,35 @@ manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
94102
runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
95103
runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
96104

97-
runResultStream :: ResultStream a -> IO (Maybe (Either String a), Maybe (Either String a), Maybe (Either String a), Maybe (Either String a))
98-
runResultStream (ResultStream k) = k $ \act -> (,,,) <$> act <*> act <*> act <*> act
105+
runResultStream :: ResultStream a
106+
-> IO ( Maybe (Either String a)
107+
, Maybe (Either String a)
108+
, Maybe (Either String a)
109+
, Maybe (Either String a))
110+
runResultStream (ResultStream k)
111+
= k $ \act -> (,,,) <$> act <*> act <*> act <*> act
99112

100113
streamSpec :: Spec
101114
streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
102115

103-
it "Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
116+
it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
104117
Right res <- runClient getGetNL baseUrl
105118
let jra = Just (Right alice)
106119
jrb = Just (Right bob)
107120
runResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
108121

109-
it "Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
122+
it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
110123
Right res <- runClient getGetNS baseUrl
111124
let jra = Just (Right alice)
112125
jrb = Just (Right bob)
113126
runResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
127+
128+
it "streams in constant memory" $ \(_, baseUrl) -> do
129+
Right (ResultStream res) <- runClient getGetALot baseUrl
130+
let consumeNChunks n = replicateM_ n (res void)
131+
consumeNChunks 900
132+
memUsed <- currentBytesUsed <$> getGCStats
133+
memUsed `shouldSatisfy` (< (megabytes 20))
134+
135+
megabytes :: Num a => a -> a
136+
megabytes n = n * (1000 ^ 2)

0 commit comments

Comments
 (0)