packages feed

http2-client 0.3.0.2 → 0.4.0.0

raw patch · 4 files changed

+204/−165 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Network.HTTP2.Client: linkHttp2Client :: Http2Client -> IO ()
- Network.HTTP2.Client: newHttp2Client :: HostName -> PortNumber -> Int -> Int -> ClientParams -> SettingsList -> IO Http2Client
- Network.HTTP2.Client: wrapFrameClient :: Http2FrameConnection -> Int -> Int -> SettingsList -> IO Http2Client
+ Network.HTTP2.Client: RemoteSentGoAwayFrame :: !StreamId -> !ErrorCodeId -> !ByteString -> RemoteSentGoAwayFrame
+ Network.HTTP2.Client: data RemoteSentGoAwayFrame
+ Network.HTTP2.Client: defaultGoAwayHandler :: GoAwayHandler
+ Network.HTTP2.Client: ignoreFallbackHandler :: FallBackFrameHandler
+ Network.HTTP2.Client: instance GHC.Exception.Exception Network.HTTP2.Client.RemoteSentGoAwayFrame
+ Network.HTTP2.Client: instance GHC.Show.Show Network.HTTP2.Client.RemoteSentGoAwayFrame
+ Network.HTTP2.Client: linkAsyncs :: Http2Client -> IO ()
+ Network.HTTP2.Client: runHttp2Client :: Http2FrameConnection -> Int -> Int -> SettingsList -> GoAwayHandler -> FallBackFrameHandler -> (Http2Client -> IO a) -> IO a
+ Network.HTTP2.Client: type FallBackFrameHandler = (FrameHeader, FramePayload) -> IO ()
+ Network.HTTP2.Client: type GoAwayHandler = RemoteSentGoAwayFrame -> IO ()

Files

README.md view
@@ -177,7 +177,7 @@  ### Things that will likely change the API -I think the fundamental are right but the following needs tweaking:+I think the fundamentals are right but the following needs tweaking:  - function to reset a stream will likely be blocking until a RST/EndStream is   received so that all DATA frames are accounted for in the flow-control system@@ -208,6 +208,3 @@     CPU/latency-wise if the concurrency is high - consider most performant functions for HTTP2.HPACK encoding/decoding   * currently using naive function 'Network.HPACK.encodeHeader'-- consider using a typeclass or a way to change the frame client-- we need a story for IO exceptions-- we need a story for instrumentation
app/Main.hs view
@@ -2,19 +2,19 @@ {-# LANGUAGE RecordWildCards   #-} module Main where -import           Control.Concurrent (forkIO, threadDelay)+import           Control.Concurrent (forkIO, myThreadId, throwTo, threadDelay) import           Control.Concurrent.Async (async, waitAnyCancel) import           Control.Monad (forever, when, void) import           Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as ByteString import           Data.Default.Class (def) import           Data.Maybe (fromMaybe)+import           Data.Monoid ((<>)) import           Data.Time.Clock (diffUTCTime, getCurrentTime) import qualified Network.HTTP2 as HTTP2 import qualified Network.TLS as TLS import qualified Network.TLS.Extra.Cipher as TLS import           Options.Applicative-import           Data.Monoid ((<>)) import           System.IO  import Network.HTTP2.Client@@ -182,50 +182,51 @@                       return hdrFrame                 }           }-    conn <- case _verboseDebug of-        Verbose ->-            wrapFrameClient wrappedFrameConn _encoderBufsize _decoderBufsize conf-        NonVerbose ->-            wrapFrameClient frameConn _encoderBufsize _decoderBufsize conf-    linkHttp2Client conn-    _addCredit (_incomingFlowControl conn) _initialWindowKick-    _ <- forkIO $ forever $ do-            updated <- _updateWindow $ _incomingFlowControl conn-            when updated $ timePrint ("sending flow-control update" :: String)-            threadDelay _interFlowControlUpdates+    parentThread <- myThreadId+    let withConn = case _verboseDebug of+            Verbose ->+                runHttp2Client wrappedFrameConn _encoderBufsize _decoderBufsize conf defaultGoAwayHandler printUnhandledFrame+            NonVerbose ->+                runHttp2Client frameConn _encoderBufsize _decoderBufsize conf (throwTo parentThread) ignoreFallbackHandler -    _ <- forkIO $ when (_interPingDelay > 0) $ forever $ do-        threadDelay _interPingDelay-        (t0, t1, pingReply) <- ping conn _pingTimeout "pingpong"-        timePrint $ ("ping-reply:" :: String, pingReply, diffUTCTime t1 t0)+    withConn $ \conn -> do+      linkAsyncs conn+      _addCredit (_incomingFlowControl conn) _initialWindowKick+      _ <- forkIO $ forever $ do+              updated <- _updateWindow $ _incomingFlowControl conn+              when updated $ timePrint ("sending flow-control update" :: String)+              threadDelay _interFlowControlUpdates -    let go 0 idx = timePrint $ "done worker: " <> show idx-        go n idx = do-            _ <- (withHttp2Stream conn $ \stream ->-                    let initStream = do-                            _ <- async $ onPushPromise stream (ppHandler n idx)-                            headers stream headersPairs headersFlags-                        handler streamINFlowControl streamOUTFlowControl = do-                            timePrint $ "stream started " <> show (idx, n)-                            _ <- async $ dataPostFunction conn-                                                          (_outgoingFlowControl conn)-                                                          stream-                                                          streamOUTFlowControl-                            ret <-  fromStreamResult <$> waitStream stream streamINFlowControl-                            either (\e -> timePrint e) (dump MainFile _path n idx _downloadPrefix) ret-                            timePrint $ "stream ended " <> show (idx, n)-                    in StreamDefinition initStream handler)-            go (n - 1) idx+      _ <- forkIO $ when (_interPingDelay > 0) $ forever $ do+          threadDelay _interPingDelay+          (t0, t1, pingReply) <- ping conn _pingTimeout "pingpong"+          timePrint $ ("ping-reply:" :: String, pingReply, diffUTCTime t1 t0) -    _ <- waitAnyCancel =<< traverse (async . go _numberQueries) [1 .. _concurrentQueriesCount]+      let go 0 idx = timePrint $ "done worker: " <> show idx+          go n idx = do+              _ <- (withHttp2Stream conn $ \stream ->+                      let initStream = do+                              _ <- async $ onPushPromise stream (ppHandler n idx)+                              headers stream headersPairs headersFlags+                          handler streamINFlowControl streamOUTFlowControl = do+                              timePrint $ "stream started " <> show (idx, n)+                              _ <- async $ dataPostFunction conn+                                                            (_outgoingFlowControl conn)+                                                            stream+                                                            streamOUTFlowControl+                              ret <-  fromStreamResult <$> waitStream stream streamINFlowControl+                              either (\e -> timePrint e) (dump MainFile _path n idx _downloadPrefix) ret+                              timePrint $ "stream ended " <> show (idx, n)+                      in StreamDefinition initStream handler)+              go (n - 1) idx -    when (_finalDelay > 0) (threadDelay _finalDelay)+      _ <- waitAnyCancel =<< traverse (async . go _numberQueries) [1 .. _concurrentQueriesCount] -    _gtfo conn HTTP2.NoError _finalMessage+      when (_finalDelay > 0) (threadDelay _finalDelay) -    _close conn -    return ()+      _gtfo conn HTTP2.NoError _finalMessage+      _close conn   where     tlsParams = TLS.ClientParams {           TLS.clientWantSessionResume    = Nothing@@ -273,3 +274,6 @@ timePrint x = do     tst <- getCurrentTime     ByteString.hPutStrLn stderr $ ByteString.pack $ show (tst, x)++printUnhandledFrame :: FallBackFrameHandler+printUnhandledFrame (fh,fp) = timePrint ("UNHANDLED:"::String, fh, fp)
http2-client.cabal view
@@ -1,5 +1,5 @@ name:                http2-client-version:             0.3.0.2+version:             0.4.0.0 synopsis:            A native HTTP2 client library. description:         Please read the README.md at the homepage. homepage:            https://github.com/lucasdicioccio/http2-client@@ -33,7 +33,7 @@ executable http2-client-exe   hs-source-dirs:      app   main-is:             Main.hs-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -O2   build-depends:       base                      , async                      , bytestring
src/Network/HTTP2/Client.hs view
@@ -8,7 +8,7 @@ -- For higher-level primitives, please refer to Network.HTTP2.Client.Helpers . module Network.HTTP2.Client (     -- * Basics-      newHttp2Client+      runHttp2Client     , withHttp2Stream     , headers     , sendData@@ -25,11 +25,15 @@     , IncomingFlowControl(..)     , OutgoingFlowControl(..)     -- * Exceptions-    , linkHttp2Client+    , linkAsyncs+    , RemoteSentGoAwayFrame(..)+    , GoAwayHandler+    , defaultGoAwayHandler     -- * Misc.+    , FallBackFrameHandler+    , ignoreFallbackHandler     , FlagSetter     , Http2ClientAsyncs(..)-    , wrapFrameClient     , _gtfo     -- * Convenience re-exports     , module Network.HTTP2.Client.FrameConnection@@ -37,8 +41,8 @@     , module Network.TLS     ) where -import           Control.Exception (bracket, throw)-import           Control.Concurrent.Async (Async, async, race, link)+import           Control.Concurrent.Async (Async, race, withAsync, link)+import           Control.Exception (bracket, throwIO, SomeException, catch) import           Control.Concurrent.MVar (newMVar, takeMVar, putMVar) import           Control.Concurrent (threadDelay) import           Control.Concurrent.Chan (Chan, newChan, dupChan, readChan, writeChan)@@ -47,7 +51,6 @@ import qualified Data.ByteString as ByteString import           Data.IORef (IORef, newIORef, atomicModifyIORef', readIORef) import           Data.Maybe (fromMaybe)-import           Data.Monoid ((<>)) import           GHC.Exception (Exception) import           Network.HPACK as HPACK import           Network.HTTP2 as HTTP2@@ -176,8 +179,9 @@  -- | Set of Async threads running an Http2Client. ----- If you add other asyncs here, you should also modify--- 'linkHttp2Client'.+-- This asyncs are linked to the thread where the Http2Client is created.+-- If you modify this structure to add more Async, please also modify+-- 'linkAsyncs' accordingly. data Http2ClientAsyncs = Http2ClientAsyncs {     _waitSettingsAsync   :: Async (FrameHeader, FramePayload)   -- ^ Async waiting for the initial settings ACK.@@ -196,18 +200,15 @@   -- 'incomingFramesLoop'.   } --- | Links all asyncs threads of a client to the current thread.------ See 'link'.-linkHttp2Client :: Http2Client -> IO ()-linkHttp2Client client = -    let Http2ClientAsyncs{..} = _asyncs client-    in do-        link _waitSettingsAsync-        link _creditFlowsAsync-        link _HPACKAsync-        link _controlFramesAsync-        link _incomingFramesAsync+-- | Links all client's asyncs to current thread.+linkAsyncs :: Http2Client -> IO ()+linkAsyncs client =+   let Http2ClientAsyncs{..} = _asyncs client in do+           link _waitSettingsAsync+           link _creditFlowsAsync+           link _HPACKAsync+           link _controlFramesAsync+           link _incomingFramesAsync  -- | Couples client and server settings together. data ConnectionSettings = ConnectionSettings {@@ -277,25 +278,6 @@   , _applySettings    :: Int -> IO ()   } --- | Starts a new Http2Client with a remote Host/Port. TLS ClientParams are--- mandatory because we only support TLS-protected streams for now.-newHttp2Client :: HostName-               -- ^ Host to connect to.-               -> PortNumber-               -- ^ Port number to connect to (usually 443 on the web).-               -> Int-               -- ^ The buffersize for the Network.HPACK encoder.-               -> Int-               -- ^ The buffersize for the Network.HPACK decoder.-               -> ClientParams-               -- ^ The TLS client parameters (e.g., to allow some certificates).-               -> SettingsList-               -- ^ Initial SETTINGS that are sent as first frame.-               -> IO Http2Client-newHttp2Client host port encoderBufSize decoderBufSize tlsParams initSettings = do-    conn <- newHttp2FrameConnection host port tlsParams-    wrapFrameClient conn encoderBufSize decoderBufSize initSettings- -- | Starts a new stream (i.e., one HTTP request + server-pushes). -- -- You will typically call the returned 'StreamStarter' immediately to define@@ -334,12 +316,12 @@ headers :: Http2Stream -> HeaderList -> FlagSetter -> IO StreamThread headers = _headers --- | Prepares a client around a frame connection.+-- | Starts a new Http2Client around a frame connection. -- -- This is mostly useful if you want to muck around the Http2FrameConnection -- (e.g., for tracing frames) as well as for unit testing purposes. If you want -- to create a new connection you should likely use 'newHttp2Client' instead.-wrapFrameClient+runHttp2Client   :: Http2FrameConnection   -- ^ A frame connection.   -> Int@@ -348,8 +330,16 @@   -- ^ The buffersize for the Network.HPACK decoder.   -> SettingsList   -- ^ Initial SETTINGS that are sent as first frame.-  -> IO Http2Client-wrapFrameClient conn encoderBufSize decoderBufSize initSettings = do+  -> GoAwayHandler+  -- ^ Actions to run when the remote sends a GoAwayFrame+  -> FallBackFrameHandler+  -- ^ Actions to run when a control frame is not yet handled in http2-client+  -- lib (e.g., PRIORITY frames).+  -> (Http2Client -> IO a)+  -- ^ Actions to run on the client.+  -> IO a+runHttp2Client conn encoderBufSize decoderBufSize initSettings goAwayHandler fallbackHandler mainHandler = do+    let _close = closeConnection conn     -- prepare hpack contexts     hpackEncoder <- do         let strategy = (HPACK.defaultEncodeStrategy { HPACK.useHuffman = True })@@ -371,87 +361,82 @@     -- Initial thread receiving server frames.     maxReceivedStreamId  <- newIORef 0     serverFrames <- newChan-    aIncoming <- async $ incomingFramesLoop conn serverFrames maxReceivedStreamId--    -- Thread handling control frames.-    settings  <- newIORef defaultConnectionSettings-    controlFrames <- dupChan serverFrames-    aControl <- async $ incomingControlFramesLoop controlFrames settings hpackEncoder ackPing ackSettings--    -- Thread handling push-promises and headers frames serializing the buffers.-    serverStreamFrames <- dupChan serverFrames-    serverHeaders <- newChan-    hpackDecoder <- do-        dt <- newDynamicTableForDecoding HPACK.defaultDynamicTableSize decoderBufSize-        return dt--    creditFrames <- dupChan serverFrames--    _outgoingFlowControl <- newOutgoingFlowControl settings 0 creditFrames-    _incomingFlowControl <- newIncomingFlowControl settings controlStream--    serverPushPromises <- newChan--    aHPACK <- async $ incomingHPACKFramesLoop serverStreamFrames-                                              serverHeaders-                                              serverPushPromises-                                              hpackDecoder+    let incomingLoop = incomingFramesLoop conn serverFrames maxReceivedStreamId+    withAsync incomingLoop $ \aIncoming -> do -    dataFrames <- dupChan serverFrames-    aCredit <- async $ creditDataFramesLoop _incomingFlowControl dataFrames+      -- Thread handling control frames.+      settings  <- newIORef defaultConnectionSettings+      controlFrames <- dupChan serverFrames+      let controlLoop = incomingControlFramesLoop controlFrames settings hpackEncoder ackPing ackSettings goAwayHandler fallbackHandler+      withAsync controlLoop $ \aControl -> do+        -- Thread handling push-promises and headers frames serializing the buffers.+        serverStreamFrames <- dupChan serverFrames+        serverHeaders <- newChan+        hpackDecoder <- do+            dt <- newDynamicTableForDecoding HPACK.defaultDynamicTableSize decoderBufSize+            return dt -    conccurentStreams <- newIORef 0-    let _startStream getWork = do-            maxConcurrency <- fromMaybe 100 . maxConcurrentStreams . _serverSettings-               <$> readIORef settings-            roomNeeded <- atomicModifyIORef' conccurentStreams-                (\n -> if n < maxConcurrency then (n + 1, 0) else (n, 1 + n - maxConcurrency))-            if roomNeeded > 0-            then-                return $ Left $ TooMuchConcurrency roomNeeded-            else do-                cont <- withClientStreamId $ \sid -> do-                    initializeStream conn-                                     settings-                                     serverFrames-                                     serverHeaders-                                     serverPushPromises-                                     hpackEncoder-                                     sid-                                     getWork-                v <- cont-                atomicModifyIORef' conccurentStreams (\n -> (n - 1, ()))-                return $ Right v+        creditFrames <- dupChan serverFrames -    let _ping dat = do-            -- Need to dupChan before sending the query to avoid missing a fast-            -- answer if the network is fast.-            pingFrames <- dupChan serverFrames-            sendPingFrame controlStream id dat-            return $ waitFrame (isPingReply dat) pingFrames-    let _settings settslist = do-            -- Much like _ping, we need to dupChan before sending the query.-            pingFrames <- dupChan serverFrames-            sendSettingsFrame controlStream id settslist-            return $ do-                ret <- waitFrame isSettingsReply pingFrames-                atomicModifyIORef' settings-                    (\(ConnectionSettings cli srv) ->-                        (ConnectionSettings (HTTP2.updateSettings cli settslist) srv, ()))-                return ret-    let _goaway err errStr = do-            sId <- readIORef maxReceivedStreamId-            sendGTFOFrame controlStream sId err errStr+        _outgoingFlowControl <- newOutgoingFlowControl settings 0 creditFrames+        _incomingFlowControl <- newIncomingFlowControl settings controlStream -    let _paylodSplitter = settingsPayloadSplitter <$> readIORef settings+        serverPushPromises <- newChan -    aSettings <- async =<< _settings initSettings+        let hpackLoop = incomingHPACKFramesLoop serverStreamFrames serverHeaders serverPushPromises hpackDecoder+        withAsync hpackLoop $ \aHPACK -> do+          dataFrames <- dupChan serverFrames+          let creditLoop = creditDataFramesLoop _incomingFlowControl dataFrames+          withAsync creditLoop $ \aCredit -> do+            conccurentStreams <- newIORef 0+            let _startStream getWork = do+                    maxConcurrency <- fromMaybe 100 . maxConcurrentStreams . _serverSettings+                       <$> readIORef settings+                    roomNeeded <- atomicModifyIORef' conccurentStreams+                        (\n -> if n < maxConcurrency then (n + 1, 0) else (n, 1 + n - maxConcurrency))+                    if roomNeeded > 0+                    then+                        return $ Left $ TooMuchConcurrency roomNeeded+                    else do+                        cont <- withClientStreamId $ \sid -> do+                            initializeStream conn+                                             settings+                                             serverFrames+                                             serverHeaders+                                             serverPushPromises+                                             hpackEncoder+                                             sid+                                             getWork+                        v <- cont+                        atomicModifyIORef' conccurentStreams (\n -> (n - 1, ()))+                        return $ Right v -    let _asyncs = Http2ClientAsyncs aSettings aCredit aHPACK aControl aIncoming+            let _ping dat = do+                    -- Need to dupChan before sending the query to avoid missing a fast+                    -- answer if the network is fast.+                    pingFrames <- dupChan serverFrames+                    sendPingFrame controlStream id dat+                    return $ waitFrame (isPingReply dat) pingFrames+            let _settings settslist = do+                    -- Much like _ping, we need to dupChan before sending the query.+                    pingFrames <- dupChan serverFrames+                    sendSettingsFrame controlStream id settslist+                    return $ do+                        ret <- waitFrame isSettingsReply pingFrames+                        atomicModifyIORef' settings+                            (\(ConnectionSettings cli srv) ->+                                (ConnectionSettings (HTTP2.updateSettings cli settslist) srv, ()))+                        return ret+            let _goaway err errStr = do+                    sId <- readIORef maxReceivedStreamId+                    sendGTFOFrame controlStream sId err errStr -    let _close = closeConnection conn+            let _paylodSplitter = settingsPayloadSplitter <$> readIORef settings -    return $ Http2Client{..}+            settsIO <- _settings initSettings+            withAsync settsIO $ \aSettings -> do+                let _asyncs = Http2ClientAsyncs aSettings aCredit aHPACK aControl aIncoming+                mainHandler $ Http2Client{..}  initializeStream   :: Exception e@@ -521,12 +506,31 @@   -> Chan (FrameHeader, Either HTTP2Error FramePayload)   -> IORef StreamId   -> IO ()-incomingFramesLoop conn frames maxReceivedStreamId = forever $ do+incomingFramesLoop conn frames maxReceivedStreamId = delayException $ forever $ do     frame@(fh, _) <- next conn     -- Remember highest streamId.     atomicModifyIORef' maxReceivedStreamId (\n -> (max n (streamId fh), ()))     writeChan frames frame +-- | Runs an action, rethrowing exception 50ms later.+--+-- In a context where asynchronous are likely to occur this function gives a+-- chance to other threads to do some work before Async linking reaps them all.+--+-- In particular, servers are likely to close their TCP connection soon after+-- sending a GoAwayFrame and we want to give a better chance to clients to+-- observe the GoAwayFrame in their handlers to distinguish GoAwayFrames+-- followed by TCP disconnection and plain TCP resets. As a result, this+-- function is mostly used to delay 'incomingFramesLoop'. A more involved+-- and future design will be to inline the various loop-processes for+-- incomingFramesLoop and GoAwayHandlers in a same thread (e.g., using+-- pipe/conduit to retain composability).+delayException :: IO a -> IO a+delayException act = act `catch` slowdown+  where+    slowdown :: SomeException -> IO a+    slowdown e = threadDelay 50000 >> throwIO e+ incomingControlFramesLoop   :: Exception e   => Chan (FrameHeader, Either e FramePayload)@@ -534,8 +538,10 @@   -> HpackEncoderContext   -> (ByteString -> IO ())   -> IO ()+  -> GoAwayHandler+  -> FallBackFrameHandler   -> IO ()-incomingControlFramesLoop frames settings hpackEncoder ackPing ackSettings = forever $ do+incomingControlFramesLoop frames settings hpackEncoder ackPing ackSettings goAwayHandler fallbackHandler = forever $ do     controlFrame@(fh, payload) <- waitFrameWithStreamId 0 frames     case payload of         (SettingsFrame settsList)@@ -556,12 +562,44 @@                 ignore "PingFrame replies waited for in the requestor thread"         (WindowUpdateFrame _ )  ->                 ignore "connection-wide WindowUpdateFrame waited for in OutgoingFlowControl threads"-        _                   -> putStrLn ("UNHANDLED frame: " <> show controlFrame)+        (GoAwayFrame lastSid errCode reason)  ->+             goAwayHandler $ RemoteSentGoAwayFrame lastSid errCode reason +        _                   ->+             fallbackHandler controlFrame+   where     ignore :: String -> IO ()     ignore _ = return () +-- | A fallback handler for frames.+type FallBackFrameHandler = (FrameHeader, FramePayload) -> IO ()++-- | Default FallBackFrameHandler that ignores frames.+ignoreFallbackHandler :: FallBackFrameHandler+ignoreFallbackHandler = const $ pure ()++-- | A Handler for exceptional circumstances.+type GoAwayHandler = RemoteSentGoAwayFrame -> IO ()++-- | Default GoAwayHandler throws a 'RemoteSentGoAwayFrame' in the current+-- thread.+--+-- A probably sharper handler if you want to abruptly stop any operation is to+-- get the 'ThreadId' of the main client thread and using+-- 'Control.Exception.Base.throwTo'.+--+-- There's an inherent race condition when receiving a GoAway frame because the+-- server will likely close the connection which will lead to TCP errors as+-- well.+defaultGoAwayHandler :: GoAwayHandler+defaultGoAwayHandler = throwIO++-- | An exception thrown when the server sends a GoAwayFrame.+data RemoteSentGoAwayFrame = RemoteSentGoAwayFrame !StreamId !ErrorCodeId !ByteString+  deriving Show+instance Exception RemoteSentGoAwayFrame+ -- | We currently need a specific loop for crediting streams because a client -- user may programmatically reset and stop listening for a stream and stop -- calling waitData (which credits streams).@@ -874,7 +912,7 @@   where     loop = do         (fHead, fPayload) <- readChan chan-        let dat = either throw id fPayload+        dat <- either throwIO pure fPayload         if test fHead dat         then return (fHead, dat)         else loop