git.iko.soy/distributed-process
on master help sign in
Continue work on error handling

Extended testEarlyDisconnect, which exposes some unimplemented error handling in the transport API. That's next to fix.

commit305bd0ac0e5d97a778815e914e66f86588fa6c82 authorEdsko de Vries <edsko@edsko.net> dateThu 10 May 2012, 21:00 (14 years ago) parent62133a7
6 files changed+352−90
network-transport-2/src/Network/Transport.hs+10−5 network-transport-2/src/Network/Transport/TCP.hs+87−54 network-transport-2/tests/TestAuxiliary.hs+6−1 network-transport-2/tests/TestTCP.hs+86−28 network-transport-2/tests/TestTransport.hs+2−2 network-transport-2/tests/Traced.hs+161
network-transport-2/src/Network/Transport.hs+10−5
@@ -16,7 +16,7 @@ module Network.Transport ( -- * Types
1616 , NewMulticastGroupErrorCode(..)
1717 , ResolveMulticastGroupErrorCode(..)
1818 , SendErrorCode(..)
19− , EventErrorCode(..)
19+ , ErrorEventInfo(..)
2020 ) where
2121
2222import Data.ByteString (ByteString)
@@ -70,7 +70,7 @@ data Event =
7070 -- | Received multicast
7171 | ReceivedMulticast MulticastAddress [ByteString]
7272 -- | Error that caused a bunch of connections to close (possibly none)
73− | ErrorEvent EventErrorCode [ConnectionId]
73+ | ErrorEvent ErrorEventInfo
7474 deriving Show
7575
7676-- | Connection IDs enable receivers to distinguish one connection from another.
@@ -122,6 +122,7 @@ instance Error (FailedWith error) where
122122-- | Errors during the creation of an endpoint (currently, there are none)
123123data NewEndPointErrorCode =
124124 NewEndPointTransportFailure -- ^ Transport-wide failure
125+ deriving Show
125126
126127-- | Connection failure
127128data ConnectErrorCode =
@@ -148,7 +149,11 @@ data SendErrorCode =
148149 deriving Show
149150
150151-- | Error codes used when reporting errors to endpoints (through receive)
151−data EventErrorCode =
152− EventErrorEndPointClosed -- ^ Endpoint was closed (manually or because of an error)
153− | EventErrorTransportFailure -- ^ Transport-wide fatal error
152+data ErrorEventInfo =
153+ -- | Endpoint was closed (manually or because of an error)
154+ ErrorEventEndPointClosed [ConnectionId]
155+ -- | Transport-wide fatal error
156+ | ErrorEventTransportFailure
157+ -- | Connection to a remote endpoint was lost
158+ | ErrorEventConnectionLost EndPointAddress [ConnectionId]
154159 deriving Show
network-transport-2/src/Network/Transport/TCP.hs+87−54
@@ -10,6 +10,7 @@ module Network.Transport.TCP ( -- * Main API
1010 createTransport
1111 , -- * TCP specific functionality
1212 EndPointId
13+ , encodeEndPointAddress
1314 , decodeEndPointAddress
1415 , ControlHeader(..)
1516 , ConnectionRequestResponse(..)
@@ -237,6 +238,12 @@ data LocalState = LocalState { _nextConnectionId :: ConnectionId
237238--
238239-- Since we don't do concurrent reads from the same socket we don't need to
239240-- take the lock when reading from the socket.
241+--
242+-- Moreover, we insist on the invariant (INV-CLOSE) that whenever we put an
243+-- endpoint in closed state we remove that endpoint from localConnections
244+-- first, so that if a concurrent thread reads the mvar, finds EndPointClosed,
245+-- and then looks up the endpoint in localConnections it is guaranteed to
246+-- either find a different remote endpoint, or else none at all.
240247
241248data RemoteState = RemoteEndPointInvalid (FailedWith ConnectErrorCode)
242249 | RemoteEndPointValid RemoteConnection
@@ -258,14 +265,14 @@ data ControlHeader =
258265 | CloseConnection -- ^ Tell the remote endpoint we will no longer be using a connection
259266 | ControlResponse -- ^ Respond to a control request _from_ the remote endpoint
260267 | CloseSocket -- ^ Request to close the connection (see module description)
261− deriving (Enum, Bounded)
268+ deriving (Enum, Bounded, Show)
262269
263270-- Response sent by /B/ to /A/ when /A/ tries to connect
264271data ConnectionRequestResponse =
265272 ConnectionRequestAccepted -- ^ /B/ accepts the connection
266273 | ConnectionRequestEndPointInvalid -- ^ /A/ requested an invalid endpoint
267274 | ConnectionRequestCrossed -- ^ /A/s request crossed with a request from /B/ (see protocols)
268− deriving (Enum, Bounded)
275+ deriving (Enum, Bounded, Show)
269276
270277--------------------------------------------------------------------------------
271278-- Top-level functionality --
@@ -393,7 +400,7 @@ apiCloseEndPoint transport ourEndPoint = do
393400 -- calls will block. We could change this so that all subsequent calls to
394401 -- receive return an error, but this would mean checking for some state on
395402 -- every call to receive, which is an unnecessary overhead.
396− writeChan (localChannel ourEndPoint) (ErrorEvent EventErrorEndPointClosed incoming)
403+ writeChan (localChannel ourEndPoint) (ErrorEvent $ ErrorEventEndPointClosed incoming)
397404 where
398405 remoteSocketOf :: RemoteEndPoint -> MaybeT IO N.Socket
399406 remoteSocketOf theirEndPoint = do
@@ -452,7 +459,7 @@ createLocalEndPoint transport = do
452459 }
453460 modifyMVar (transportState transport) $ \st -> do
454461 let ix = fromIntegral $ Map.size (st ^. localEndPoints)
455− let addr = encodeEndPointAddress transport ix
462+ let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) ix
456463 let localEndPoint = LocalEndPoint { localAddress = addr
457464 , localChannel = chan
458465 , localState = state
@@ -472,7 +479,7 @@ requestConnectionTo ourEndPoint theirAddress = ErrorT go
472479 go = do
473480 let ourState = localState ourEndPoint
474481 (theirEndPoint, isNew) <- modifyMVar ourState $ \st ->
475− case st ^. connectionTo theirAddress of
482+ case st ^. localConnectionTo theirAddress of
476483 Just theirEndPoint ->
477484 return (st, (theirEndPoint, False))
478485 Nothing -> do
@@ -480,7 +487,7 @@ requestConnectionTo ourEndPoint theirAddress = ErrorT go
480487 let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress
481488 , remoteState = theirState
482489 }
483− return (connectionTo theirAddress ^= Just theirEndPoint $ st, (theirEndPoint, True))
490+ return (localConnectionTo theirAddress ^= Just theirEndPoint $ st, (theirEndPoint, True))
484491
485492 -- If this is a new endpoint, fork a thread to listen for incoming
486493 -- connections. We don't want to do this while we hold the lock, because
@@ -574,14 +581,14 @@ connectToRemoteEndPoint (ourEndPoint, theirEndPoint) = do
574581 -- waiting on the result of this call to connect will get the
575582 -- RemoteEndPointInvalid; subsequent threads will initiate a new
576583 -- connection requests.
577− modifyMVar_ ourState $ return . (connectionTo theirAddress ^= Nothing)
584+ modifyMVar_ ourState $ return . (localConnectionTo theirAddress ^= Nothing)
578585 putMVar theirState (RemoteEndPointInvalid (invalidAddress "Invalid endpoint"))
579586 N.sClose sock
580587 Right (ConnectionRequestCrossed, sock) -> do
581588 N.sClose sock
582589 Left err -> do
583590 -- See comment above
584− modifyMVar_ ourState $ return . (connectionTo theirAddress ^= Nothing)
591+ modifyMVar_ ourState $ return . (localConnectionTo theirAddress ^= Nothing)
585592 putMVar theirState (RemoteEndPointInvalid err)
586593 -- TODO: this isn't quite right; socket *might* have been opened here,
587594 -- in which case we should close it
@@ -613,9 +620,9 @@ connectToRemoteEndPoint (ourEndPoint, theirEndPoint) = do
613620 EndPointAddress ourAddress = localAddress ourEndPoint
614621
615622-- | Encode end point address
616−encodeEndPointAddress :: TCPTransport -> EndPointId -> EndPointAddress
617−encodeEndPointAddress transport ix = EndPointAddress . BSC.pack $
618− transportHost transport ++ ":" ++ transportPort transport ++ ":" ++ show ix
623+encodeEndPointAddress :: N.HostName -> N.ServiceName -> EndPointId -> EndPointAddress
624+encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $
625+ host ++ ":" ++ port ++ ":" ++ show ix
619626
620627-- | Decode end point address
621628-- TODO: This uses regular expression parsing, which is nice, but unnecessary
@@ -677,7 +684,7 @@ handleConnectionRequest transport sock = do
677684 request <- tryIO $ do
678685 ourEndPointId <- recvInt32 sock
679686 theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock
680− let ourAddress = encodeEndPointAddress transport ourEndPointId
687+ let ourAddress = encodeEndPointAddress (transportHost transport) (transportPort transport) ourEndPointId
681688 ourEndPoint <- (^. localEndPointAt ourAddress) <$> readMVar (transportState transport)
682689 return (theirAddress, ourEndPoint)
683690 case request of
@@ -686,13 +693,13 @@ handleConnectionRequest transport sock = do
686693 N.sClose sock
687694 Right (theirAddress, Just ourEndPoint) -> forkEndPointThread ourEndPoint $ do
688695 (crossed, theirEndPoint) <- modifyMVar (localState ourEndPoint) $ \st ->
689− case st ^. connectionTo theirAddress of
696+ case st ^. localConnectionTo theirAddress of
690697 Nothing -> do
691698 theirState <- newEmptyMVar
692699 let theirEndPoint = RemoteEndPoint { remoteAddress = theirAddress
693700 , remoteState = theirState
694701 }
695− return (connectionTo theirAddress ^= Just theirEndPoint $ st, (False, theirEndPoint))
702+ return (localConnectionTo theirAddress ^= Just theirEndPoint $ st, (False, theirEndPoint))
696703 Just theirEndPoint ->
697704 return (st, (localAddress ourEndPoint < theirAddress, theirEndPoint))
698705 if crossed
@@ -733,28 +740,10 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do
733740 _ ->
734741 error "handleIncomingMessages RELY violation"
735742
736− tryIO $ go sock
737−
738− -- Normally, there would be no unclosed connections, but if the client
739− -- exits prematurely there might be
740− mUnclosedConnections <- modifyMVar theirState $ \st ->
741− case st of
742− RemoteEndPointInvalid _ ->
743− error "handleIncomingMessages RELY violation"
744− RemoteEndPointValid remoteConn ->
745− return (RemoteEndPointClosed, Just $ remoteConn ^. remoteIncoming)
746− RemoteEndPointClosing _ _ ->
747− return (RemoteEndPointClosed, Nothing)
748− RemoteEndPointClosed ->
749− return (st, Nothing)
750−
751− -- TODO: we need to remote this endpoint from localConnections at this point
752−
753− case mUnclosedConnections of
754− Nothing -> return ()
755− Just unclosedConnections ->
756− writeChan ourChannel (ErrorEvent EventErrorEndPointClosed (IntSet.elems unclosedConnections))
757−
743+ mCleanExit <- tryIO $ go sock
744+ case mCleanExit of
745+ Right () -> return ()
746+ Left err -> prematureExit sock err
758747 where
759748 -- Dispatch
760749 go :: N.Socket -> IO ()
@@ -835,24 +824,45 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do
835824 -- Close the socket (if we don't have any outgoing connections)
836825 closeSocket :: N.Socket -> IO Bool
837826 closeSocket sock = do
838− didClose <- modifyMVar theirState $ \st ->
827+ -- We need to check if we can close the socket (that is, if we don't have
828+ -- any outgoing connections), and once we are sure that we can put the
829+ -- endpoint in Closed state. However, by INV-CLOSE we can only put the
830+ -- endpoint in Closed state once we remove the endpoint from our local
831+ -- connections. But we can do that only once we are sure that we can
832+ -- close the endpoint. Catch-22. We can resolve the catch-22 by locking
833+ -- *both* our local state and the endpoint state, but at the cost of
834+ -- introducing a double lock and all the associated perils, or by putting
835+ -- the remote endpoint in Closing state first. We opt for the latter.
836+ canClose <- modifyMVar theirState $ \st ->
839837 case st of
840− RemoteEndPointValid remoteConn | remoteConn ^. remoteOutgoing == 0 -> do
841− sendOn remoteConn [encodeInt32 CloseSocket]
842− N.sClose sock
843− return (RemoteEndPointClosed, True)
844− RemoteEndPointClosing resolved _ -> do
845− -- If the socket is already in closing state, then we have already
846− -- sent a CloseSocket request; hence, we don't need to do it again
847− -- at this point
848− putMVar resolved ()
849− N.sClose sock
850− return (RemoteEndPointClosed, True)
838+ RemoteEndPointValid remoteConn -> do
839+ -- TODO: We check only for outgoing connections here. That is
840+ -- correct, but we need to makke sure that we send the right event
841+ -- to our endpoint for any incoming connections that are still live
842+ -- at this point.
843+ if remoteConn ^. remoteOutgoing == 0
844+ then do
845+ -- TODO: code duplication with closeIfUnused
846+ resolved <- newEmptyMVar
847+ sendOn remoteConn [encodeInt32 CloseSocket]
848+ return (RemoteEndPointClosing resolved remoteConn, Just resolved)
849+ else
850+ return (st, Nothing)
851+ RemoteEndPointClosing resolved _ ->
852+ return (st, Just resolved)
851853 _ ->
852− return (st, False)
853− when didClose $ modifyMVar_ ourState $ return . (connectionTo theirAddr ^= Nothing)
854− return didClose
855−
854+ error "handleIncomingConnections RELY violation"
855+
856+ case canClose of
857+ Nothing ->
858+ return False
859+ Just resolved -> do
860+ modifyMVar_ ourState $ return . (localConnectionTo theirAddr ^= Nothing)
861+ modifyMVar_ theirState $ return . const RemoteEndPointClosed
862+ N.sClose sock
863+ putMVar resolved ()
864+ return True
865+
856866 -- Read a message and output it on the endPoint's channel
857867 readMessage :: N.Socket -> ConnectionId -> IO ()
858868 readMessage sock connId = recvWithLength sock >>= writeChan ourChannel . Received connId
@@ -863,6 +873,29 @@ handleIncomingMessages (ourEndPoint, theirEndPoint) = do
863873 theirState = remoteState theirEndPoint
864874 theirAddr = remoteAddress theirEndPoint
865875
876+ -- Deal with a premature exit
877+ prematureExit :: N.Socket -> IOException -> IO ()
878+ prematureExit sock _ = do
879+ N.sClose sock
880+ modifyMVar_ ourState $ return . (localConnectionTo theirAddr ^= Nothing)
881+ mUnclosedConnections <- modifyMVar theirState $ \st ->
882+ case st of
883+ RemoteEndPointInvalid _ ->
884+ error "handleIncomingMessages RELY violation"
885+ RemoteEndPointValid remoteConn ->
886+ return (RemoteEndPointClosed, Just $ remoteConn ^. remoteIncoming)
887+ RemoteEndPointClosing _ _ ->
888+ return (RemoteEndPointClosed, Nothing)
889+ RemoteEndPointClosed ->
890+ return (st, Nothing)
891+
892+ -- We send the connection lost message even if unclosedConnections is the
893+ -- empty set, because *outgoing* connections will be broken too now
894+ case mUnclosedConnections of
895+ Nothing ->
896+ return ()
897+ Just unclosedConnections -> writeChan ourChannel . ErrorEvent $
898+ ErrorEventConnectionLost (remoteAddress theirEndPoint) (IntSet.elems unclosedConnections)
866899
867900
868901-- | Get the next connection ID
@@ -911,8 +944,8 @@ localEndPointAt addr = localEndPoints >>> mapLens addr
911944pendingCtrlRequestsAt :: ControlRequestId -> Lens LocalState (Maybe (MVar [ByteString]))
912945pendingCtrlRequestsAt ix = pendingCtrlRequests >>> intMapLens (fromIntegral ix)
913946
914−connectionTo :: EndPointAddress -> Lens LocalState (Maybe RemoteEndPoint)
915−connectionTo addr = localConnections >>> mapLens addr
947+localConnectionTo :: EndPointAddress -> Lens LocalState (Maybe RemoteEndPoint)
948+localConnectionTo addr = localConnections >>> mapLens addr
916949
917950remoteOutgoing :: Lens RemoteConnection Int
918951remoteOutgoing = lens _remoteOutgoing (\cs conn -> conn { _remoteOutgoing = cs })
network-transport-2/tests/TestAuxiliary.hs+6−1
@@ -1,4 +1,9 @@
1−module TestAuxiliary (runTest, runTests, forkTry) where
1+module TestAuxiliary ( -- Running tests
2+ runTest
3+ , runTests
4+ -- Writing tests
5+ , forkTry
6+ ) where
27
38import Prelude hiding (catch)
49import Control.Concurrent (myThreadId, forkIO, ThreadId, throwTo)
network-transport-2/tests/TestTCP.hs+86−28
@@ -1,22 +1,23 @@
1+{-# LANGUAGE RebindableSyntax #-}
12module Main where
23
3−import Prelude hiding (catch)
4+import Prelude hiding (catch, (>>=), (>>), return, fail)
45import TestTransport (testTransport)
56import TestAuxiliary (forkTry, runTests)
67import Network.Transport
7−import Network.Transport.TCP (createTransport)
8+import Network.Transport.TCP (createTransport, encodeEndPointAddress)
89import Data.Int (Int32)
910import Data.Maybe (fromJust)
1011import Control.Concurrent (threadDelay)
1112import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar, readMVar, isEmptyMVar)
12−import Control.Monad (replicateM, guard)
13+import Control.Monad (replicateM)
1314import Control.Applicative ((<$>))
1415import Network.Transport.TCP ( decodeEndPointAddress
1516 , EndPointId
1617 , ControlHeader(..)
1718 , ConnectionRequestResponse(..)
1819 )
19−import Network.Transport.Internal (encodeInt32, prependLength, tlog)
20+import Network.Transport.Internal (encodeInt32, prependLength, tlog, tryIO)
2021import Network.Transport.Internal.TCP (recvInt32)
2122import qualified Network.Socket as N ( getAddrInfo
2223 , socket
@@ -32,41 +33,97 @@ import qualified Network.Socket as N ( getAddrInfo
3233 , ServiceName
3334 )
3435import Network.Socket.ByteString (sendMany)
36+import Data.String (fromString)
37+import Traced
38+
39+instance Show (MVar a) where
40+ show _ = "<<mvar>>"
41+
42+instance Show EndPoint where
43+ show _ = "<<endpoint>>"
44+
45+instance Show Transport where
46+ show _ = "<<transport>>"
47+
48+instance Show Connection where
49+ show _ = "<<connection>>"
50+
51+trlog :: String -> Traced ()
52+trlog = liftIO . tlog
3553
3654-- Test that the server gets a ConnectionClosed message when the client closes
3755-- the socket without sending an explicit control message to the server first
3856testEarlyDisconnect :: IO ()
3957testEarlyDisconnect = do
58+ clientAddr <- newEmptyMVar
4059 serverAddr <- newEmptyMVar
4160 serverDone <- newEmptyMVar
42− Right transport <- createTransport "127.0.0.1" "8081"
43−
44− forkTry $ server transport serverAddr serverDone
45− forkTry $ client transport serverAddr
61+
62+ tlog "testEarlyDisconnect"
63+ forkTry $ server serverAddr clientAddr serverDone
64+ forkTry $ client serverAddr clientAddr
4665
4766 takeMVar serverDone
4867 where
49− server :: Transport -> MVar (N.HostName, N.ServiceName, EndPointId) -> MVar () -> IO ()
50− server transport serverAddr serverDone = do
68+ server :: MVar (N.HostName, N.ServiceName, EndPointId) -> MVar EndPointAddress -> MVar () -> IO ()
69+ server serverAddr clientAddr serverDone = do
70+ tlog "Server"
71+ Right transport <- createTransport "127.0.0.1" "8081"
5172 Right endpoint <- newEndPoint transport
5273 putMVar serverAddr (fromJust . decodeEndPointAddress . address $ endpoint)
53− ConnectionOpened _ _ _ <- receive endpoint
54− ConnectionClosed _ <- receive endpoint
74+ theirAddr <- readMVar clientAddr
75+
76+ runTraced $ do
77+ -- First test: they connect to us, then drop the connection
78+ ConnectionOpened cid _ addr <- liftIO $ receive endpoint
79+ True <- return $ addr == theirAddr
80+
81+ ErrorEvent (ErrorEventConnectionLost addr [cid']) <- liftIO $ receive endpoint
82+ True <- return $ addr == theirAddr && cid' == cid
83+
84+ -- Second test: after they dropped their connection to us, we now try to
85+ -- establish a connection to them. This should re-establish the broken
86+ -- TCP connection.
87+ trlog "Trying to connect to client"
88+ Right conn <- liftIO $ connect endpoint theirAddr ReliableOrdered
89+
90+
91+ return ()
92+
5593 putMVar serverDone ()
5694
57− client :: Transport -> MVar (N.HostName, N.ServiceName, EndPointId) -> IO ()
58− client transport serverAddr = do
59− Right endpoint <- newEndPoint transport
60− let EndPointAddress myAddress = address endpoint
61−
95+{-
96+ -- To test the connection, we do a simple ping test; as before, however,
97+ -- the remote client won't close the connection nicely but just closes
98+ -- the socket
99+ do
100+ send conn ["ping"]
101+ cid <- expect endpoint (\(ConnectionOpened cid _ addr) -> (addr == theirAddr, cid))
102+ expect' endpoint (\(Received cid' [msg]) -> cid' == cid && msg == "pong")
103+ expect' endpoint (\(ErrorEvent (ErrorEventConnectionLost addr [cid'])) -> addr == theirAddr && cid' == cid)
104+
105+ -- TODO: we now need to decide (and test) and should happen on a subsequent 'send'
106+
107+-}
108+
109+
110+
111+ client :: MVar (N.HostName, N.ServiceName, EndPointId) -> MVar EndPointAddress -> IO ()
112+ client serverAddr clientAddr = do
113+ tlog "Client"
114+ let ourAddress = encodeEndPointAddress "127.0.0.1" "8082" 0
115+ putMVar clientAddr ourAddress
116+ (_, _, endPointIx) <- readMVar serverAddr
117+
118+
119+
62120 -- Connect to the server
63121 addr:_ <- N.getAddrInfo Nothing (Just "127.0.0.1") (Just "8081")
64122 sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol
65123 N.setSocketOption sock N.ReuseAddr 1
66124 N.connect sock (N.addrAddress addr)
67125
68− (_, _, endPointIx) <- readMVar serverAddr
69− sendMany sock (encodeInt32 endPointIx : prependLength [myAddress])
126+ sendMany sock (encodeInt32 endPointIx : prependLength [endPointAddressToByteString ourAddress])
70127
71128 -- Wait for acknowledgement
72129 ConnectionRequestAccepted <- toEnum <$> recvInt32 sock
@@ -87,24 +144,24 @@ testInvalidAddress = do
87144
88145-- | Test connecting to invalid or non-existing endpoints
89146testInvalidConnect :: IO ()
90−testInvalidConnect = do
91− Right transport <- createTransport "127.0.0.1" "8083"
92− Right endpoint <- newEndPoint transport
147+testInvalidConnect = runTraced $ do
148+ Right transport <- liftIO $ createTransport "127.0.0.1" "8083"
149+ Right endpoint <- liftIO $ newEndPoint transport
93150
94151 -- Syntax error in the endpoint address
95− Left (FailedWith ConnectInvalidAddress _) <-
152+ Left (FailedWith ConnectInvalidAddress _) <- liftIO $
96153 connect endpoint (EndPointAddress "InvalidAddress") ReliableOrdered
97154
98155 -- Syntax connect, but invalid hostname (TCP address lookup failure)
99− Left (FailedWith ConnectInvalidAddress _) <-
156+ Left (FailedWith ConnectInvalidAddress _) <- liftIO $
100157 connect endpoint (EndPointAddress "invalidHost:port:0") ReliableOrdered
101158
102159 -- TCP address correct, but nobody home at that address
103− Left (FailedWith ConnectFailed _) <-
160+ Left (FailedWith ConnectFailed _) <- liftIO $
104161 connect endpoint (EndPointAddress "127.0.0.1:9000:0") ReliableOrdered
105162
106163 -- Valid TCP address but invalid endpoint number
107− Left (FailedWith ConnectInvalidAddress _) <-
164+ Left (FailedWith ConnectInvalidAddress _) <- liftIO $
108165 connect endpoint (EndPointAddress "127.0.0.1:8083:1") ReliableOrdered
109166
110167 return ()
@@ -276,7 +333,7 @@ testBlockAfterCloseSocket = do
276333 -- respond by sending a ConnectionRequest)
277334 forkTry $ do
278335 recvInt32 sock :: IO Int32
279− isEmptyMVar unblocked >>= guard . not
336+ isEmptyMVar unblocked >>= (guard . not)
280337 putMVar clientDone ()
281338
282339 threadDelay 1000000
@@ -289,7 +346,8 @@ testBlockAfterCloseSocket = do
289346
290347main :: IO ()
291348main = do
292− runTests [ ("EarlyDisconnect", testEarlyDisconnect)
349+ tryIO $ runTests
350+ [ ("EarlyDisconnect", testEarlyDisconnect)
293351 , ("InvalidAddress", testInvalidAddress)
294352 , ("InvalidConnect", testInvalidConnect)
295353 , ("IgnoreCloseSocket", testIgnoreCloseSocket)
network-transport-2/tests/TestTransport.hs+2−2
@@ -37,7 +37,7 @@ echoServer endpoint = do
3737 ReceivedMulticast _ _ ->
3838 -- Ignore
3939 go cs
40− ErrorEvent _ _ ->
40+ ErrorEvent _ ->
4141 fail (show event)
4242
4343-- | Wait for an event, throw an exception if the given predicate returns false
@@ -298,7 +298,7 @@ collect endPoint numEvents = go numEvents Map.empty Map.empty
298298 go (n - 1) (Map.adjust (msg :) cid open) closed
299299 ReceivedMulticast _ _ ->
300300 fail "Unexpected multicast"
301− ErrorEvent _ _ ->
301+ ErrorEvent _ ->
302302 fail "Unexpected error"
303303
304304-- | Open connection, close it, then reopen it
network-transport-2/tests/Traced.hsnew file+161
@@ -0,0 +1,161 @@
1+{-# LANGUAGE RebindableSyntax #-}
2+-- | Monad to trace all terms that get bound
3+--
4+-- Examples:
5+--
6+-- > test1 :: IO Int
7+-- > test1 = runTraced $ do
8+-- > Left x <- return (Left 1 :: Either Int Int)
9+-- > Left y <- return (Left 2 :: Either Int Int)
10+-- > return (x + y)
11+--
12+-- outputs
13+--
14+-- > 3
15+--
16+-- as expected. Things get more interesting when we have a pattern match failure:
17+--
18+-- > test2 :: IO Int
19+-- > test2 = runTraced $ do
20+-- > Left x <- return (Left 1 :: Either Int Int)
21+-- > Right y <- return (Left 2 :: Either Int Int)
22+-- > return (x + y)
23+--
24+-- outputs
25+--
26+-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:96:3-9
27+-- > Trace:
28+-- > Left 1
29+-- > Left 2
30+-- > )
31+--
32+-- The module also exports a 'guard' function which is used in similar fashion
33+-- to the 'guard' from "Control.Monad":
34+--
35+-- > test3 :: IO Int
36+-- > test3 = runTraced $ do
37+-- > Left x <- return (Left 1 :: Either Int Int)
38+-- > Left y <- return (Left 2 :: Either Int Int)
39+-- > guard (x == 3)
40+-- > return (x + y)
41+--
42+-- outputs
43+--
44+-- > *** Exception: user error (Guard failed
45+-- > Trace:
46+-- > Left 1
47+-- > Left 2
48+-- > )
49+--
50+-- Note that we get no line-number info this way; we can fix this by saying
51+--
52+-- > test4 :: IO Int
53+-- > test4 = runTraced $ do
54+-- > Left x <- return (Left 1 :: Either Int Int)
55+-- > Left y <- return (Left 2 :: Either Int Int)
56+-- > True <- return (x == 3)
57+-- > return (x + y)
58+--
59+-- which outputs
60+--
61+-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:110:3-6
62+-- > Trace:
63+-- > Left 1
64+-- > Left 2
65+-- > False
66+-- > )
67+--
68+-- The module also exports a 'liftIO', and does not trace ignored unit values (from '>>'):
69+--
70+-- > test5 :: IO Int
71+-- > test5 = runTraced $ do
72+-- > Left x <- return (Left 1 :: Either Int Int)
73+-- > liftIO $ putStrLn "hi"
74+-- > Left y <- return (Left 2 :: Either Int Int)
75+-- > True <- return (y == 1)
76+-- > return (x + y)
77+--
78+-- outputs
79+--
80+-- > hi
81+-- > *** Exception: user error (Pattern match failure in do expression at Traced.hs:137:3-6
82+-- > Trace:
83+-- > Left 1
84+-- > Left 2
85+-- > False
86+-- > )
87+--
88+-- Every Monad is also a MonadS, but not vice versa.
89+
90+module Traced ( -- Subclass of monads where bind is defined only for showable arguments
91+ MonadS(..)
92+ , return
93+ , (>>=)
94+ , (>>)
95+ , fail
96+ , guard
97+ -- MonadS instance that shows a trace of all bound terms when an error occurs
98+ , Traced
99+ , runTraced
100+ , liftIO
101+ ) where
102+
103+import Prelude hiding ((>>=), return, fail, catch, (>>))
104+import qualified Prelude
105+import Data.String (fromString)
106+
107+class MonadS m where
108+ returnS :: a -> m a
109+ bindS :: Show a => m a -> (a -> m b) -> m b
110+ failS :: String -> m a
111+ seqS :: Show a => m a -> m b -> m b
112+
113+instance MonadS IO where
114+ returnS = Prelude.return
115+ bindS = (Prelude.>>=)
116+ failS = Prelude.fail
117+ seqS = (Prelude.>>)
118+
119+newtype Traced a = Traced { unTraced :: IO ([String], Either String a) }
120+
121+instance MonadS Traced where
122+ returnS x = Traced $ Prelude.return ([], Right x)
123+ x `bindS` f = Traced $ unTraced x Prelude.>>= \(trace, result) ->
124+ case result of
125+ Left err -> Prelude.return (trace, Left err)
126+ Right a -> unTraced (f a) Prelude.>>= \(trace', result') ->
127+ Prelude.return (trace ++ show a : trace', result')
128+ x `seqS` y = Traced $ unTraced x Prelude.>>= \(trace, result) ->
129+ case result of
130+ Left err -> Prelude.return (trace, Left err)
131+ Right a -> unTraced y Prelude.>>= \(trace', result') ->
132+ Prelude.return (trace ++ trace', result')
133+ failS err = Traced $ Prelude.return ([], Left err)
134+
135+runTraced :: Traced a -> IO a
136+runTraced (Traced x) = x Prelude.>>= \(trace, ma) ->
137+ case ma of
138+ Left err -> Prelude.fail $ err ++ "\nTrace:\n" ++ showTrace trace
139+ Right a -> Prelude.return a
140+
141+showTrace :: [String] -> String
142+showTrace = unlines . map (\xs -> ' ' : ' ' : xs)
143+
144+guard :: MonadS m => Bool -> m ()
145+guard True = returnS ()
146+guard False = failS "Guard failed"
147+
148+(>>=) :: (MonadS m, Show a) => m a -> (a -> m b) -> m b
149+(>>=) = bindS
150+
151+(>>) :: (MonadS m, Show a) => m a -> m b -> m b
152+(>>) = seqS
153+
154+return :: MonadS m => a -> m a
155+return = returnS
156+
157+fail :: MonadS m => String -> m a
158+fail = failS
159+
160+liftIO :: IO a -> Traced a
161+liftIO x = Traced $ x Prelude.>>= \a -> Prelude.return ([], Right a)