http2-client 0.1.0.1 → 0.2.0.0
raw patch · 5 files changed
+119/−53 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Network.HTTP2.Client: TooMuchConcurrency :: Int -> TooMuchConcurrency
+ Network.HTTP2.Client: [_getStreamRoomNeeded] :: TooMuchConcurrency -> Int
+ Network.HTTP2.Client: [_waitPushPromise] :: Http2Stream -> PushPromiseHandler -> IO ()
+ Network.HTTP2.Client: newtype TooMuchConcurrency
+ Network.HTTP2.Client.Helpers: TimedOut :: TimedOut
+ Network.HTTP2.Client.Helpers: data TimedOut
+ Network.HTTP2.Client.Helpers: fromStreamResult :: StreamResult -> Either ErrorCode StreamResponse
+ Network.HTTP2.Client.Helpers: instance GHC.Show.Show Network.HTTP2.Client.Helpers.TimedOut
+ Network.HTTP2.Client.Helpers: onPushPromise :: Http2Stream -> PushPromiseHandler -> IO ()
+ Network.HTTP2.Client.Helpers: type StreamResponse = (HeaderList, ByteString)
- Network.HTTP2.Client: Http2Client :: (ByteString -> IO (IO (FrameHeader, FramePayload))) -> (SettingsList -> IO ()) -> (ErrorCodeId -> ByteString -> IO ()) -> (forall a. StreamStarter a) -> IncomingFlowControl -> OutgoingFlowControl -> IO PayloadSplitter -> Http2Client
+ Network.HTTP2.Client: Http2Client :: (ByteString -> IO (IO (FrameHeader, FramePayload))) -> (SettingsList -> IO (IO (FrameHeader, FramePayload))) -> (ErrorCodeId -> ByteString -> IO ()) -> (forall a. StreamStarter a) -> IncomingFlowControl -> OutgoingFlowControl -> IO PayloadSplitter -> Http2Client
- Network.HTTP2.Client: Http2Stream :: (HeaderList -> (FrameFlags -> FrameFlags) -> IO StreamThread) -> (Priority -> IO ()) -> (ErrorCodeId -> IO ()) -> IO (FrameHeader, StreamId, Either ErrorCode HeaderList) -> IO (FrameHeader, Either ErrorCode ByteString) -> ((FrameFlags -> FrameFlags) -> ByteString -> IO ()) -> Http2Stream
+ Network.HTTP2.Client: Http2Stream :: (HeaderList -> (FrameFlags -> FrameFlags) -> IO StreamThread) -> (Priority -> IO ()) -> (ErrorCodeId -> IO ()) -> IO (FrameHeader, StreamId, Either ErrorCode HeaderList) -> IO (FrameHeader, Either ErrorCode ByteString) -> ((FrameFlags -> FrameFlags) -> ByteString -> IO ()) -> (PushPromiseHandler -> IO ()) -> Http2Stream
- Network.HTTP2.Client: [_settings] :: Http2Client -> SettingsList -> IO ()
+ Network.HTTP2.Client: [_settings] :: Http2Client -> SettingsList -> IO (IO (FrameHeader, FramePayload))
- Network.HTTP2.Client: newHttp2Client :: HostName -> PortNumber -> Int -> Int -> ClientParams -> SettingsList -> PushPromiseHandler a -> IO Http2Client
+ Network.HTTP2.Client: newHttp2Client :: HostName -> PortNumber -> Int -> Int -> ClientParams -> SettingsList -> IO Http2Client
- Network.HTTP2.Client: type PushPromiseHandler a = StreamId -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> IO a
+ Network.HTTP2.Client: type PushPromiseHandler = StreamId -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> IO ()
- Network.HTTP2.Client.Helpers: ping :: Int -> ByteString -> Http2Client -> IO PingReply
+ Network.HTTP2.Client.Helpers: ping :: Http2Client -> Int -> ByteString -> IO PingReply
- Network.HTTP2.Client.Helpers: type PingReply = (UTCTime, UTCTime, Either () (FrameHeader, FramePayload))
+ Network.HTTP2.Client.Helpers: type PingReply = (UTCTime, UTCTime, Either TimedOut (FrameHeader, FramePayload))
Files
- README.md +10/−11
- app/Main.hs +6/−5
- http2-client.cabal +2/−2
- src/Network/HTTP2/Client.hs +59/−31
- src/Network/HTTP2/Client/Helpers.hs +42/−4
README.md view
@@ -104,8 +104,13 @@ move in the "looser direction" (e.g., more concurrency) should be applied _after_ ACK-ing the SETTINGS frame. -For simplicity of implementation, the current design apply SETTINGS:-- (client prefs) before sending SETTINGS, irrespective of ACK -- we should change this+The current design apply SETTINGS:+- (client prefs) after receiving a ACK for sent SETTINGS, you get the choice to+ wait for an ACK or wait in a thread, but you must wait for an ACK to apply+ changed settings (the `_settings` function will return an IO to wait for the+ ACK and apply settings). Note that the initial SETTINGS change frame is+ waited for in a thread without library's user intervention (if you feel+ strongly against this choice, please open a bug). - (server prefs) immediately after receiving and hence before sending ACK-SETTINGS Fortunately, changing settings mid-stream is probably a rare behavior and the@@ -126,8 +131,8 @@ A number of HTTP2 features are currently hardcoded: - PINGs are replied-to immediately (i.e., a server could hog a connection with PINGs)-- received SETTINGS are acknowledged immediately after being taken into account-- sent SETTINGS are implemented immediately before sending, and not after acking+- the initial SETTINGS frame sent to the server is waited-for in a separate+ thread, settings are applied to the connection when the server ACKs the frame - flow-control from DATA frames is decremented immediately when received (in a separate thread) rather than when consumed from the client - similarly, flow-control re-increment every DATA received as soon as it is@@ -160,8 +165,6 @@ I think the fundamental are right but the following needs tweaking: -- onPushPromise handlers will be requested when opening new streams rather than- when opening new connections - 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 - need a way to hook custom flow-control algorithms@@ -186,11 +189,7 @@ ### Various TODO -- modify client SETTINGS only when acknowledged- * currently make use of new client settings before the server has time to- know them, this can lead to errors due to inconsistent view of settings- between sender/receiver-- consider a beter frame-subscription mechanism than broadcast wake-up+- consider a better frame-subscription mechanism than broadcast wake-up * current system of dupChan everything is prone to errors and may be costly CPU/latency-wise if the concurrency is high - consider most performant functions for HTTP2.HPACK encoding/decoding
app/Main.hs view
@@ -111,9 +111,9 @@ , (":authority", ByteString.pack _host) ] <> _extraHeaders - let onPushPromise _ stream streamFlowControl _ = void $ forkIO $ do+ let ppHandler _ stream streamFlowControl _ = void $ forkIO $ do timePrint ("push stream started" :: String)- waitStream stream streamFlowControl >>= timePrint+ waitStream stream streamFlowControl >>= timePrint . fromStreamResult timePrint ("push stream ended" :: String) let conf = [ (HTTP2.SettingsMaxFrameSize, _settingsMaxFrameSize)@@ -125,7 +125,7 @@ ] timePrint conf - conn <- newHttp2Client _host _port _encoderBufsize _decoderBufsize tlsParams conf onPushPromise+ conn <- newHttp2Client _host _port _encoderBufsize _decoderBufsize tlsParams conf _addCredit (_incomingFlowControl conn) _initialWindowKick _ <- forkIO $ forever $ do updated <- _updateWindow $ _incomingFlowControl conn@@ -134,7 +134,7 @@ _ <- forkIO $ when (_interPingDelay > 0) $ forever $ do threadDelay _interPingDelay- (t0, t1, pingReply) <- ping _pingTimeout "pingpong" conn+ (t0, t1, pingReply) <- ping conn _pingTimeout "pingpong" timePrint $ ("ping-reply:" :: String, pingReply, diffUTCTime t1 t0) let go 0 idx = timePrint $ "done worker: " <> show idx@@ -142,8 +142,9 @@ _ <- (_startStream conn $ \stream -> let initStream = _headers stream headersPairs (HTTP2.setEndStream) handler streamFlowControl _ = do+ _ <- async $ onPushPromise stream ppHandler timePrint $ "stream started " <> show (idx, n)- waitStream stream streamFlowControl >>= timePrint+ waitStream stream streamFlowControl >>= timePrint . fromStreamResult timePrint $ "stream ended " <> show (idx, n) in StreamDefinition initStream handler) go (n - 1) idx
http2-client.cabal view
@@ -1,7 +1,7 @@ name: http2-client-version: 0.1.0.1+version: 0.2.0.0 synopsis: A native HTTP2 client library.-description: Please read the README.ms at the homepage.+description: Please read the README.md at the homepage. homepage: https://github.com/lucasdicioccio/http2-client license: BSD3 license-file: LICENSE
src/Network/HTTP2/Client.hs view
@@ -10,6 +10,7 @@ -- * Starting streams , StreamDefinition(..) , StreamStarter+ , TooMuchConcurrency(..) , StreamThread , Http2Stream(..) -- * Sending data for POSTs.@@ -30,7 +31,7 @@ import Control.Concurrent.MVar (newMVar, takeMVar, putMVar) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.Chan (Chan, newChan, dupChan, readChan, writeChan)-import Control.Monad (forever, when, forM_)+import Control.Monad (forever, void, when, forM_) import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString import Data.IORef (IORef, newIORef, atomicModifyIORef', readIORef)@@ -139,8 +140,11 @@ -- first call to this IO will return if a reply is received. Hence we -- recommend wrapping this IO in an Async (e.g., with @race (threadDelay -- timeout)@.)- , _settings :: SettingsList -> IO ()- -- ^ Sends a SETTINGS.+ , _settings :: SettingsList -> IO (IO (FrameHeader, FramePayload))+ -- ^ Sends a SETTINGS. Returns an IO to wait for a settings reply. No timeout+ -- is provided. Only the first call to this IO will return if a reply is+ -- received. Hence we recommend wrapping this IO in an Async (e.g., with+ -- @race (threadDelay timeout)@.) , _goaway :: ErrorCodeId -> ByteString -> IO () -- ^ Sends a GOAWAY. , _startStream :: forall a. StreamStarter a@@ -202,6 +206,7 @@ -- and hence does not respect the RFC if sending large blocks. Use 'sendData' -- to chunk and send naively according to server\'s preferences. This function -- can be useful if you intend to handle the framing yourself.+ , _waitPushPromise :: PushPromiseHandler -> IO () } -- | Handler upon receiving a PUSH_PROMISE from the server.@@ -213,8 +218,8 @@ -- The StreamId corresponds to the parent stream as PUSH_PROMISEs are tied to a -- client-initiated stream. Longer term we may move passing this handler to the -- '_startStream' instead of 'newHttp2Client' (as it is for now).-type PushPromiseHandler a =- StreamId -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> IO a+type PushPromiseHandler =+ StreamId -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> IO () -- | Helper to carry around the HPACK encoder for outgoing header blocks.. data HpackEncoderContext = HpackEncoderContext {@@ -236,10 +241,8 @@ -- ^ The TLS client parameters (e.g., to allow some certificates). -> SettingsList -- ^ Initial SETTINGS that are sent as first frame.- -> PushPromiseHandler a- -- ^ Action to perform when a server sends a PUSH_PROMISE. -> IO Http2Client-newHttp2Client host port encoderBufSize decoderBufSize tlsParams initSettings handlePPStream = do+newHttp2Client host port encoderBufSize decoderBufSize tlsParams initSettings = do -- network connection conn <- newHttp2FrameConnection host port tlsParams @@ -283,13 +286,12 @@ _outgoingFlowControl <- newOutgoingFlowControl settings 0 creditFrames _incomingFlowControl <- newIncomingFlowControl settings controlStream + serverPushPromises <- newChan+ _ <- forkIO $ incomingHPACKFramesLoop serverStreamFrames serverHeaders- hpackEncoder+ serverPushPromises hpackDecoder- conn- settings- handlePPStream dataFrames <- dupChan serverFrames _ <- forkIO $ creditDataFramesLoop _incomingFlowControl dataFrames@@ -309,6 +311,7 @@ settings serverFrames serverHeaders+ serverPushPromises hpackEncoder sid getWork@@ -323,17 +326,22 @@ sendPingFrame controlStream id dat return $ waitFrame (isPingReply dat) pingFrames let _settings settslist = do- atomicModifyIORef' settings- (\(ConnectionSettings cli srv) ->- (ConnectionSettings (HTTP2.updateSettings cli settslist) srv, ()))+ -- 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 _paylodSplitter = settingsPayloadSplitter <$> readIORef settings - _settings initSettings+ _ <- forkIO . void =<< _settings initSettings return $ Http2Client{..} @@ -343,17 +351,19 @@ -> IORef ConnectionSettings -> Chan (FrameHeader, Either e FramePayload) -> Chan (FrameHeader, StreamId, Either ErrorCode HeaderList)+ -> Chan (StreamId, StreamId) -> HpackEncoderContext -> StreamId -> (Http2Stream -> StreamDefinition a) -> IO (IO a)-initializeStream conn settings serverFrames serverHeaders hpackEncoder sid getWork = do+initializeStream conn settings serverFrames serverHeaders serverPushPromises hpackEncoder sid getWork = do let frameStream = makeFrameClientStream conn sid -- Register interest in frames. frames <- dupChan serverFrames credits <- dupChan serverFrames headers <- dupChan serverHeaders+ pushPromises <- dupChan serverPushPromises -- Builds a flow-control context. incomingStreamFlowControl <- newIncomingFlowControl settings frameStream@@ -377,6 +387,18 @@ let _sendDataChunk = sendDataFrame frameStream let _rst = sendResetFrame frameStream let _prio = sendPriorityFrame frameStream+ let _waitPushPromise ppHandler = do+ (_,ppSid) <- waitPushPromiseWithParentStreamId sid pushPromises+ let mkStreamActions stream = StreamDefinition (return CST) (ppHandler sid stream)+ ppCont <- initializeStream conn+ settings+ serverFrames+ serverHeaders+ serverPushPromises+ hpackEncoder+ ppSid+ mkStreamActions+ ppCont let streamActions = getWork $ Http2Stream{..} @@ -454,13 +476,10 @@ :: Exception e => Chan (FrameHeader, Either e FramePayload) -> Chan (FrameHeader, StreamId, Either ErrorCode HeaderList)- -> HpackEncoderContext+ -> Chan (StreamId, StreamId) -> DynamicTable- -> Http2FrameConnection- -> IORef ConnectionSettings- -> PushPromiseHandler a -> IO ()-incomingHPACKFramesLoop frames headers hpackEncoder hpackDecoder conn settings handlePPStream = forever $ do+incomingHPACKFramesLoop frames headers pushPromises hpackDecoder = forever $ do (fh, fp) <- waitFrameWithTypeId [ FrameRSTStream , FramePushPromise , FrameHeaders@@ -469,15 +488,7 @@ (sId, pattern) <- case fp of PushPromiseFrame sid hbf -> do let parentSid = HTTP2.streamId fh- let mkStreamActions stream = StreamDefinition (return CST) (handlePPStream parentSid stream)- cont <- initializeStream conn- settings- frames- headers- hpackEncoder- sid- mkStreamActions- _ <- cont -- TODO: inline+ writeChan pushPromises (parentSid, sid) return (sid, Right hbf) HeadersFrame _ hbf -> -- TODO: handle priority return (HTTP2.streamId fh, Right hbf)@@ -712,6 +723,10 @@ isPingReply datSent _ (PingFrame datRcv) = datSent == datRcv isPingReply _ _ _ = False +isSettingsReply :: FrameHeader -> FramePayload -> Bool+isSettingsReply fh (SettingsFrame _) = HTTP2.testAck (flags fh)+isSettingsReply _ _ = False+ waitHeadersWithStreamId :: StreamId -> Chan (FrameHeader, StreamId, t)@@ -730,4 +745,17 @@ tuple@(fH, sId, headers) <- readChan chan if test fH sId headers then return tuple+ else loop++waitPushPromiseWithParentStreamId+ :: StreamId+ -> Chan (StreamId, StreamId)+ -> IO (StreamId, StreamId)+waitPushPromiseWithParentStreamId sid chan =+ loop+ where+ loop = do+ pair@(parentSid,_) <- readChan chan+ if parentSid == sid+ then return pair else loop
src/Network/HTTP2/Client/Helpers.hs view
@@ -7,21 +7,44 @@ import Data.ByteString (ByteString) import Control.Concurrent (threadDelay) import Control.Concurrent.Async (race)+import Control.Monad (forever) import Network.HTTP2.Client -type PingReply = (UTCTime, UTCTime, Either () (HTTP2.FrameHeader, HTTP2.FramePayload))+-- | Opaque type to express an action which timed out.+data TimedOut = TimedOut+ deriving Show -ping :: Int -> ByteString -> Http2Client -> IO PingReply-ping timeout msg conn = do+-- | Result for a 'ping'.+type PingReply = (UTCTime, UTCTime, Either TimedOut (HTTP2.FrameHeader, HTTP2.FramePayload))++-- | Performs a 'ping' and waits for a reply up to a given timeout (in+-- microseconds).+ping :: Http2Client+ -- ^ client connection+ -> Int+ -- ^ timeout in microseconds+ -> ByteString+ -- ^ 8-bytes message to uniquely identify the reply+ -> IO PingReply+ping conn timeout msg = do t0 <- getCurrentTime waitPing <- _ping conn msg- pingReply <- race (threadDelay timeout) waitPing+ pingReply <- race (threadDelay timeout >> return TimedOut) waitPing t1 <- getCurrentTime return $ (t0, t1, pingReply) +-- | Result containing the unpacked headers and all frames received in on a+-- stream. See 'StreamResponse' and 'fromStreamResult' to get a higher-level+-- utility. type StreamResult = (Either HTTP2.ErrorCode HPACK.HeaderList, [Either HTTP2.ErrorCode ByteString]) +-- | An HTTP2 response, once fully received, is made of headers and a payload.+type StreamResponse = (HPACK.HeaderList, ByteString)++-- | Wait for a stream until completion.+--+-- This function is fine if you don't want to consume results in chunks. waitStream :: Http2Stream -> IncomingFlowControl -> IO StreamResult waitStream stream streamFlowControl = do (_,_,hdrs) <- _waitHeaders stream@@ -36,3 +59,18 @@ else do _ <- _updateWindow $ streamFlowControl moredata (x:xs)++-- | Converts a StreamResult to a StramResponse, stopping at the first error+-- using the `Either HTTP2.ErrorCode` monad.+fromStreamResult :: StreamResult -> Either HTTP2.ErrorCode StreamResponse+fromStreamResult (headersE, chunksE) = do+ headers <- headersE+ chunks <- sequence chunksE+ return (headers, mconcat chunks)++-- | Sequentially wait for every push-promise with a handler.+--+-- This function runs forever and you should wrap it with 'withAsync'.+onPushPromise :: Http2Stream -> PushPromiseHandler -> IO ()+onPushPromise stream handler = forever $ do+ _waitPushPromise stream handler