Skip to content

Commit b05341b

Browse files
Merge pull request #167 from edsko/edsko/receiver-done
Ensure sender notices when receiver has terminated
2 parents 721e872 + 61c186e commit b05341b

6 files changed

Lines changed: 57 additions & 47 deletions

File tree

Network/HTTP2/Client/Run.hs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,15 @@ runH2 conf ctx runClient = do
151151
er <- race runReceiver runClient
152152
case er of
153153
Right r -> return r
154-
-- never reached because runReceiver throws an exception to exit.
155-
Left () -> throwIO ConnectionIsClosed
154+
Left err -> throwIO err
155+
156+
-- When 'runClientReceiver' terminates, it is important we give the sender
157+
-- a chance to terminate cleanly also (it's possible the client terminated
158+
-- but there are still some messages in the queue to be sent).
159+
--
160+
-- If the client terminated successfully, we ignore any other errors in the
161+
-- sender (indeed, any exception here might simply be that the background
162+
-- threads were cancelled /because/ the client terminated).
156163
runAll = snd <$> concurrently runSender runClientReceiver
157164

158165
makeStream

Network/HTTP2/H2/Context.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ data Context = Context
9090
, mySockAddr :: SockAddr
9191
, peerSockAddr :: SockAddr
9292
, threadManager :: T.ThreadManager
93-
, receiverDone :: TVar Bool
93+
, receiverDone :: TVar (Maybe SomeException)
9494
, workersDone :: STM Bool
9595
}
9696
{- FOURMOLU_ENABLE -}
@@ -138,7 +138,7 @@ newContext roleInfo Config{..} cacheSiz connRxWS mySettings timmgr mdone = do
138138
let mySockAddr = confMySockAddr
139139
let peerSockAddr = confPeerSockAddr
140140
threadManager <- T.newThreadManager timmgr
141-
receiverDone <- newTVarIO False
141+
receiverDone <- newTVarIO Nothing
142142
let workersDone = fromMaybe (T.isAllGone threadManager) mdone
143143
return Context{..}
144144
where

Network/HTTP2/H2/Receiver.hs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import qualified Data.ByteString.Char8 as C8
1919
import qualified Data.ByteString.Short as Short
2020
import qualified Data.ByteString.UTF8 as UTF8
2121
import Data.IORef
22+
import Data.Void
2223
import Network.Control
2324
import Network.HTTP.Semantics
2425
import qualified System.ThreadManager as T
@@ -45,28 +46,35 @@ headerFragmentLimit = 51200 -- 50K
4546

4647
----------------------------------------------------------------
4748

48-
frameReceiver :: Context -> Config -> IO ()
49-
frameReceiver ctx conf@Config{..} =
50-
(switch `E.catch` handler)
51-
`E.finally` atomically
52-
(writeTVar (receiverDone ctx) True)
49+
frameReceiver :: Context -> Config -> IO E.SomeException
50+
frameReceiver ctx@Context{receiverDone} conf@Config{..} =
51+
E.mask $ \unmask -> do
52+
mErr <- E.try $ unmask switch
53+
case mErr of
54+
Left err -> do
55+
atomically $ writeTVar receiverDone $ Just err
56+
return err
57+
Right x -> do
58+
absurd x -- We only terminate due to exceptions
5359
where
54-
handler ConnectionIsClosed = return ()
55-
handler e = E.throwIO e
60+
switch :: IO Void
5661
switch = do
5762
labelMe "H2 receiver"
5863
tid <- myThreadId
5964
if confReadNTimeout
6065
then
6166
loop1
6267
else
63-
void $
64-
T.withHandle (threadManager ctx) (E.throwTo tid ConnectionIsTimeout) loop2
68+
T.withHandle (threadManager ctx) (E.throwTo tid ConnectionIsTimeout) loop2
69+
70+
loop1 :: IO Void
6571
loop1 = do
6672
hd <- confReadN frameHeaderLength -- throwing an exception on timeout
6773
when (BS.null hd) $ E.throwIO ConnectionIsClosed
6874
processFrame ctx conf $ decodeFrameHeader hd
6975
loop1
76+
77+
loop2 :: T.Handle -> IO Void
7078
loop2 th = do
7179
-- If 'confReadN' is timeouted, 'ConnectionIsTimeout' is thrown
7280
-- to destroy the thread trees.

Network/HTTP2/H2/Sender.hs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import Foreign.Ptr (minusPtr, plusPtr)
1616
import Network.ByteOrder
1717
import Network.HTTP.Semantics.Client
1818
import Network.HTTP.Semantics.IO
19-
import System.ThreadManager
2019

2120
import Imports
2221
import Network.HPACK (setLimitForEncoding, toTokenHeaderTable)
@@ -59,38 +58,42 @@ updatePeerSettings Context{peerSettings, oddStreamTable, evenStreamTable} peerAl
5958
updateAllStreamTxFlow siz strms =
6059
forM_ strms $ \strm -> increaseStreamWindowSize strm siz
6160

62-
checkDone :: Context -> Int -> IO Bool
61+
checkDone :: Context -> Int -> IO (Maybe E.SomeException)
6362
checkDone Context{..} 0 = atomically $ do
6463
isEmptyC <- isEmptyTQueue controlQ
6564
isEmptyO <- isEmptyTQueue outputQ
6665
if not isEmptyC || not isEmptyO
6766
then
68-
return False
67+
return Nothing
6968
else do
70-
gone <- isAllGone threadManager
71-
unless gone retry
72-
done <- readTVar receiverDone
73-
unless done retry
74-
return True
75-
checkDone _ _ = return False
69+
recv <- readTVar receiverDone
70+
case recv of
71+
Just done ->
72+
return $ Just done
73+
_otherwise ->
74+
retry
75+
checkDone _ _ = return Nothing
7676

77-
frameSender :: Context -> Config -> IO ()
77+
frameSender :: Context -> Config -> IO E.SomeException
7878
frameSender
7979
ctx@Context{outputQ, controlQ, encodeDynamicTable, outputBufferLimit}
8080
Config{..} = do
8181
labelMe "H2 sender"
82-
loop 0
82+
loop 0 `E.catch` return
8383
where
8484
----------------------------------------------------------------
85-
loop :: Offset -> IO ()
85+
loop :: Offset -> IO E.SomeException
8686
loop off = do
87-
done <- checkDone ctx off
88-
unless done $ do
89-
x <- atomically $ dequeue off
90-
case x of
91-
C ctl -> flushN off >> control ctl >> loop 0
92-
O out -> outputAndSync out off >>= flushIfNecessary >>= loop
93-
Flush -> flushN off >> loop 0
87+
mDone <- checkDone ctx off
88+
case mDone of
89+
Just done ->
90+
return done
91+
Nothing -> do
92+
x <- atomically $ dequeue off
93+
case x of
94+
C ctl -> flushN off >> control ctl >> loop 0
95+
O out -> outputAndSync out off >>= flushIfNecessary >>= loop
96+
Flush -> flushN off >> loop 0
9497

9598
-- Flush the connection buffer to the socket, where the first 'n' bytes of
9699
-- the buffer are filled.

Network/HTTP2/H2/Stream.hs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,15 @@ readStreamState Stream{streamState} = readIORef streamState
7777

7878
closeAllStreams
7979
:: TVar OddStreamTable -> TVar EvenStreamTable -> Maybe SomeException -> IO ()
80-
closeAllStreams ovar evar mErr' = do
80+
closeAllStreams ovar evar mErr = do
8181
ostrms <- clearOddStreamTable ovar
8282
mapM_ finalize ostrms
8383
estrms <- clearEvenStreamTable evar
8484
mapM_ finalize estrms
8585
where
86+
-- We treat /every/ exception, including 'ConectionIsClosed', as abnormal
87+
-- termination: we should only report a clean termination when we receive an
88+
-- explicit @END_STREAM@ frame.
8689
finalize strm = do
8790
st <- readStreamState strm
8891
void $ tryPutMVar (streamInput strm) err
@@ -92,14 +95,6 @@ closeAllStreams ovar evar mErr' = do
9295
_otherwise ->
9396
return ()
9497

95-
mErr :: Maybe SomeException
96-
mErr = case mErr' of
97-
Just e
98-
| Just ConnectionIsClosed <- fromException e ->
99-
Nothing
100-
_otherwise ->
101-
mErr'
102-
10398
err :: Either SomeException a
10499
err = Left $ fromMaybe (toException ConnectionIsClosed) mErr
105100

Network/HTTP2/Server/Run.hs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33

44
module Network.HTTP2.Server.Run where
55

6-
import Control.Concurrent.Async (concurrently_)
6+
import Control.Concurrent.Async
77
import Control.Concurrent.STM
8-
import qualified Control.Exception as E
98
import Imports
109
import Network.Control (defaultMaxData)
1110
import Network.HTTP.Semantics.IO
@@ -128,10 +127,8 @@ runH2 conf ctx = do
128127
runReceiver = frameReceiver ctx conf
129128
runSender = frameSender ctx conf
130129
runBackgroundThreads = do
131-
er <- E.try $ concurrently_ runReceiver runSender
132-
case er of
133-
Right () -> return ()
134-
Left e -> closureServer conf ctx e
130+
e <- snd <$> concurrently runReceiver runSender
131+
closureServer conf ctx e
135132
T.stopAfter mgr runBackgroundThreads $ \res ->
136133
closeAllStreams (oddStreamTable ctx) (evenStreamTable ctx) res
137134

0 commit comments

Comments
 (0)